{"version":3,"sources":["../src/index.ts","../src/BNRExchangeHistory.ts","../src/Errors/BNRError.ts","../src/ApiClient.ts","../src/ResponseCache.ts","../src/Models/ExchangeDay.ts","../src/Models/ExchangeYear.ts"],"sourcesContent":["export * from './BNRExchangeHistory';\nexport { default } from './BNRExchangeHistory';\nexport { BNRError } from './Errors/BNRError';","import Moment from 'moment';\nimport { BNRError, InvalidBNRResponse } from './Errors/BNRError';\nimport ExchangeYear from './Models/ExchangeYear';\n\nclass BNRExchangeHistory {\n    \n    public async getRates(\n        {\n            date = new Date(),\n            invoice = false,\n        },\n    ) {\n        const maxDaysToTraverse = 25;\n    \n        for (let daysToSubtract = 0; daysToSubtract < maxDaysToTraverse; daysToSubtract++) {\n            const moment = Moment(date).subtract(daysToSubtract, 'days');\n            const exchangeYear = await this.getExchangeYear(moment);\n            const exchangeDay = exchangeYear.getDay(moment.toDate());\n        \n            if (invoice && moment.isSame(date, 'day')) {\n                continue;\n            }\n        \n            if (exchangeDay) {\n                return exchangeDay.getRates();\n            }\n        }\n    \n        throw new BNRError('Date is out of range. Needs to be between today and Jan 3rd, 2005!');\n    }\n    \n    protected validateDate(date: Moment.Moment) {\n        if (date.isAfter(Date.now(), 'day')) {\n            throw new BNRError(`The provided date \"${date.calendar()}\" appears to be in the future!`);\n        }\n    }\n    \n    protected getExchangeYear(date: Moment.Moment) {\n        this.validateDate(date);\n        \n        return ExchangeYear.for({ year: date.year() }).catch((error) => {\n            if (!(error instanceof InvalidBNRResponse)) {\n                throw error;\n            }\n        \n            if (!date.isSame(Date.now(), 'year')) {\n                throw error;\n            }\n            \n            if (date.get('dayOfYear') > 5) {\n                throw error;\n            }\n        \n            const lastYear = date.year() - 1;\n        \n            return ExchangeYear.for({ year: lastYear });\n        });\n    }\n    \n}\n\nexport default new BNRExchangeHistory();","import { AxiosResponse } from 'axios';\nimport { Year } from '../Interfaces/ExchangeYearDocument';\n\nexport class BNRError extends Error {\n    public readonly metadata?: ReturnType<typeof parseMetadata>;\n    \n    constructor(\n        public readonly message: string,\n        metadata?: RequestMetadata,\n    ) {\n        super(message);\n        \n        if (metadata) {\n            this.metadata = parseMetadata(metadata);\n        }\n        \n        if (metadata?.cause) {\n            this.stack += `\\n\\nCaused by:\\n\\n${metadata.cause.stack}`;\n        }\n    }\n}\n\n\ninterface RequestMetadata {\n    response: AxiosResponse;\n    cause?: Error;\n    year: Year;\n}\n\nfunction parseMetadata({ response, year }: RequestMetadata) {\n    return {\n        path: response.config.url,\n        baseUrl: response.config.baseURL,\n        status: `${response.status} ${response.statusText}`,\n        response: response.data,\n        requestedYear: year,\n        contentType: response.headers['content-type'],\n    };\n}\n\nexport class InvalidBNRResponse extends BNRError {\n    constructor(message: string, metadata: RequestMetadata) {\n        super(message, metadata);\n    }\n}\n\nexport class XMLParsingError extends BNRError {\n    constructor(message: string, metadata: RequestMetadata) {\n        super(message, metadata);\n    }\n}","import Axios from 'axios';\nimport { parseStringPromise as ParseXML } from 'xml2js';\nimport { InvalidBNRResponse, XMLParsingError } from './Errors/BNRError';\nimport ExchangeYearDocument, { Year } from './Interfaces/ExchangeYearDocument';\nimport { ResponseCache } from './ResponseCache';\n\nclass ApiClient {\n    protected readonly client = Axios.create({\n        baseURL: 'https://www.bnr.ro/files/xml/years/',\n    });\n    \n    protected readonly cache = new ResponseCache();\n    \n    public async getXMLForYear(year: Year): Promise<ExchangeYearDocument> {\n        if (this.cache.has(year)) {\n            return this.cache.get(year);\n        }\n        \n        return this.cache.set(year, await this.fetchRates(year));\n    }\n    \n    protected async fetchRates(year: Year): Promise<ExchangeYearDocument> {\n        const response = await this.client.get(`nbrfxrates${year}.xml`);\n        \n        const contentType = response.headers['content-type'];\n        \n        if (!contentType.includes('text/xml')) {\n            throw new InvalidBNRResponse(\n                `Received an invalid response from bnr.ro wile fetching exchange rates for year: ${year} `\n                + `This likely means there isn\\'t any exchange data for the provided year.`,\n                {\n                    response,\n                    year,\n                },\n            );\n        }\n        \n        return ParseXML(response.data).catch((cause: Error) => {\n            throw new XMLParsingError(`Unable to parse response from BNR!`, {\n                response,\n                cause,\n                year,\n            });\n        });\n    }\n}\n\nexport default new ApiClient();","import ExchangeYearDocument, { Year } from './Interfaces/ExchangeYearDocument';\n\ninterface CachedDocument {\n    document: ExchangeYearDocument,\n    lastUpdated: number;\n}\n\nexport class ResponseCache {\n    \n    protected readonly cache = new Map<Year, CachedDocument>();\n    \n    public set(year: Year, rates: ExchangeYearDocument): ExchangeYearDocument {\n        this.cache.set(year, {\n            document: rates,\n            lastUpdated: Date.now(),\n        });\n        return rates;\n    }\n    \n    /**\n     * Check if we can use the last cached response to retrieve exchange rates for the provided date.\n     * It's important that we check to make sure that we don't cache responses for the current year, as these\n     * exchange rates change every day.\n     */\n    public has(year: Year): boolean {\n        if (!this.cache.has(year)) {\n            return false;\n        }\n        \n        if (year !== new Date().getFullYear()) {\n            return true;\n        }\n        \n        const { lastUpdated } = this.cache.get(year) || {};\n        \n        if (!lastUpdated) {\n            return false;\n        }\n        \n        if (lastUpdated > (Date.now() - 60 * 60 * 1000)) {\n            return true;\n        }\n        \n        return true;\n    }\n    \n    public get(year: Year): ExchangeYearDocument {\n        const response = this.cache.get(year);\n        \n        if (!response) {\n            throw new Error('Unable to find response in cache!');\n        }\n        \n        return response.document;\n    }\n    \n    protected cacheKey(year: Year) {\n        return year.toString();\n    }\n}","import { ExchangeCube } from '../Interfaces/ExchangeYearDocument';\nimport { CurrencyCode, ExchangeRate, ExchangeRates } from '../Interfaces/ExchangeRate';\n\nexport default class ExchangeDay {\n\n    /**\n     * Exchange Day data.\n     */\n    protected readonly rawData: ExchangeCube;\n\n    /**\n     * Exchange Day constructor.\n     */\n    public constructor(data: ExchangeCube) {\n        this.rawData = data;\n    }\n\n    /**\n     * Formatted exchange object.\n     */\n    public getRates(): ExchangeRates {\n        const list = this.rawData.Rate.map((entry): [CurrencyCode, ExchangeRate] => {\n            const rate: ExchangeRate = {\n                name: entry.$.currency,\n                rate: parseFloat(entry._),\n                multiplier: parseInt(entry.$.multiplier || '1'),\n                date: this.date,\n            };\n            \n            \n            return [rate.name, rate];\n        });\n        \n        return Object.fromEntries(list) as ExchangeRates;\n    }\n\n    /**\n     * Exchange date.\n     */\n    public get date(): Date {\n        return new Date(this.rawData.$.date);\n    }\n\n}","import ApiClient from '../ApiClient';\nimport ExchangeYearDocument, { Year } from '../Interfaces/ExchangeYearDocument';\nimport ExchangeDay from './ExchangeDay';\n\nexport default class ExchangeYear {\n    \n    /**\n     * Exchange document.\n     */\n    protected readonly data: ExchangeYearDocument;\n    \n    /**\n     * Exchange Year document.\n     */\n    public constructor(data: ExchangeYearDocument) {\n        this.data = data;\n    }\n    \n    /**\n     * Exchange days for the current exchange document.\n     */\n    public get days() {\n        return this.data.DataSet.Body[0].Cube.map((cube) => new ExchangeDay(cube));\n    }\n    \n    /**\n     * Year of the current exchange rate document.\n     */\n    protected get year() {\n        return this.days[0].date.getFullYear();\n    }\n    \n    public static async for(date: { year: Year }): Promise<ExchangeYear> {\n        return new this(await ApiClient.getXMLForYear(date.year));\n    }\n    \n    /**\n     * Fetch exchange rates for the given date.\n     */\n    public getDay(date: Date) {\n        return this.days.find((exchange) => {\n            return exchange.date.toDateString() === date.toDateString();\n        });\n    }\n    \n    /**\n     * Whether or not the given date is within the same year as the current exchange year document.\n     */\n    public sameYear(date: Date) {\n        return date.getFullYear() === this.year;\n    }\n    \n}"],"mappings":"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","names":["Axios","ParseXML","Moment"]}