UNPKG

pricing-core

Version:

PricingCore - A high-precision, currency-agnostic pricing engine with BigInt precision, multiple markup strategies, multiple rounding strategies, and ISO 4217 currency support via currency-codes package

240 lines (212 loc) 8.34 kB
/** * Currency utilities for the pricing engine * Uses currency-codes package for ISO 4217 currency data * @see https://www.npmjs.com/package/currency-codes */ import currencyCodes from 'currency-codes'; /** * Currency configuration object * @typedef {Object} CurrencyConfig * @property {string} code - ISO 4217 currency code (e.g., 'USD', 'EUR', 'JPY') * @property {string} symbol - Currency symbol (e.g., '$', '€', '¥') * @property {number} decimalPlaces - Number of decimal places (e.g., 2 for USD, 0 for JPY) * @property {number} smallestUnit - Value of smallest unit in base currency (e.g., 0.01 for USD, 1 for JPY) */ /** * Currency symbols mapping for major currencies * Note: currency-codes doesn't include symbols, so we maintain a subset */ const CURRENCY_SYMBOLS = { USD: '$', EUR: '€', GBP: '£', JPY: '¥', CNY: '¥', INR: '₹', CAD: 'C$', AUD: 'A$', CHF: 'CHF', KRW: '₩', BRL: 'R$', RUB: '₽', MXN: '$', SGD: 'S$', HKD: 'HK$', SEK: 'kr', NOK: 'kr', DKK: 'kr', PLN: 'zł', CZK: 'Kč', THB: '฿', TRY: '₺', UAH: '₴', AED: 'د.إ', SAR: 'ر.س', QAR: 'ر.ق', KWD: 'د.ك', BHD: '.د.ب', OMR: 'ر.ع.', JOD: 'د.ا', LBP: 'ل.ل', ILS: '₪', IRR: '﷼', IQD: 'ع.د', LYD: 'ل.د', TND: 'د.ت', MAD: 'د.م.', EGP: '£', NGN: '₦', KES: 'KSh', GHS: '₵', ZAR: 'R', XOF: 'CFA', XAF: 'FCFA', KMF: 'CF', GNF: 'FG', CLP: '$', PYG: '₲', VUV: 'VT', XPF: '₣', XDR: 'SDR', XAU: 'Au', XAG: 'Ag', XPT: 'Pt', XPD: 'Pd', XTS: 'Test', XXX: 'No Currency' }; /** * Get all supported currencies from currency-codes package * @returns {Object} Object with currency codes as keys and CurrencyConfig as values */ export const CURRENCIES = (() => { const currencies = {}; // Get all currency codes from the package const codes = currencyCodes.codes(); codes.forEach(code => { const currencyData = currencyCodes.code(code); if (currencyData) { currencies[code] = { code: currencyData.code, symbol: CURRENCY_SYMBOLS[code] || code, // Use symbol if available, fallback to code decimalPlaces: currencyData.digits, smallestUnit: Math.pow(10, -currencyData.digits), number: currencyData.number, currency: currencyData.currency, countries: currencyData.countries }; } }); return currencies; })(); /** * Convert a decimal amount to the smallest currency unit (e.g., dollars to cents) * @param {number} amount - Amount in base currency (e.g., 2.50 for $2.50) * @param {CurrencyConfig|string} currency - Currency config or ISO code * @returns {bigint} Amount in smallest units (e.g., 250n for $2.50) */ export function toSmallestUnit(amount, currency = 'USD') { const config = typeof currency === 'string' ? CURRENCIES[currency] : currency; if (!config) { throw new Error(`Unknown currency: ${currency}`); } const multiplier = 1 / config.smallestUnit; return BigInt(Math.round(amount * multiplier)); } /** * Convert from smallest currency unit back to decimal amount * @param {bigint} units - Amount in smallest units (e.g., 250n for $2.50) * @param {CurrencyConfig|string} currency - Currency config or ISO code * @returns {number} Amount in base currency (e.g., 2.50 for $2.50) */ export function fromSmallestUnit(units, currency = 'USD') { const config = typeof currency === 'string' ? CURRENCIES[currency] : currency; if (!config) { throw new Error(`Unknown currency: ${currency}`); } return Number(units) * config.smallestUnit; } /** * Format a price in the specified currency * @param {bigint|number} amount - Amount (can be in smallest units or base currency) * @param {CurrencyConfig|string} currency - Currency config or ISO code * @param {boolean} inSmallestUnits - Whether the amount is already in smallest units * @returns {string} Formatted price string */ export function formatPrice(amount, currency = 'USD', inSmallestUnits = false) { const config = typeof currency === 'string' ? CURRENCIES[currency] : currency; if (!config) { throw new Error(`Unknown currency: ${currency}`); } let baseAmount; if (inSmallestUnits) { baseAmount = fromSmallestUnit(amount, config); } else { baseAmount = Number(amount); } return new Intl.NumberFormat('en-US', { style: 'currency', currency: config.code, minimumFractionDigits: config.decimalPlaces, maximumFractionDigits: config.decimalPlaces }).format(baseAmount); } /** * Get the step size for rounding in smallest units * @param {number} stepAmount - Step amount in base currency (e.g., 0.05 for 5¢) * @param {CurrencyConfig|string} currency - Currency config or ISO code * @returns {bigint} Step size in smallest units */ export function getStepSize(stepAmount, currency = 'USD') { const config = typeof currency === 'string' ? CURRENCIES[currency] : currency; if (!config) { throw new Error(`Unknown currency: ${currency}`); } return toSmallestUnit(stepAmount, config); } /** * Create a custom currency configuration * @param {string} code - ISO currency code * @param {string} symbol - Currency symbol * @param {number} decimalPlaces - Number of decimal places * @returns {CurrencyConfig} Custom currency configuration */ export function createCurrency(code, symbol, decimalPlaces) { const smallestUnit = Math.pow(10, -decimalPlaces); return { code, symbol, decimalPlaces, smallestUnit }; } /** * Validate if a currency code is supported * @param {string} code - ISO currency code * @returns {boolean} Whether the currency is supported */ export function isSupportedCurrency(code) { return code in CURRENCIES; } /** * Get all supported currency codes * @returns {string[]} Array of supported currency codes */ export function getSupportedCurrencies() { return Object.keys(CURRENCIES); } /** * Get currencies by decimal places * @param {number} decimalPlaces - Number of decimal places to filter by * @returns {string[]} Array of currency codes with the specified decimal places */ export function getCurrenciesByDecimalPlaces(decimalPlaces) { return Object.entries(CURRENCIES) .filter(([_, config]) => config.decimalPlaces === decimalPlaces) .map(([code, _]) => code); } /** * Get currencies by region (approximate grouping) * @returns {Object} Currencies grouped by region */ export function getCurrenciesByRegion() { return { 'North America': ['USD', 'CAD', 'MXN'], 'Europe': ['EUR', 'GBP', 'CHF', 'SEK', 'NOK', 'DKK', 'PLN', 'CZK', 'HUF', 'BGN', 'RON', 'HRK', 'RSD'], 'Asia Pacific': ['JPY', 'CNY', 'KRW', 'INR', 'SGD', 'HKD', 'TWD', 'THB', 'MYR', 'IDR', 'PHP', 'VND'], 'Latin America': ['BRL', 'ARS', 'CLP', 'COP', 'PEN', 'UYU', 'PYG'], 'Africa': ['ZAR', 'EGP', 'NGN', 'KES', 'GHS', 'MAD', 'TND', 'DZD'], 'Middle East': ['SAR', 'AED', 'QAR', 'KWD', 'BHD', 'OMR', 'JOD', 'LBP', 'ILS', 'IRR', 'IQD'], 'Oceania': ['AUD', 'NZD', 'FJD', 'PGK', 'WST', 'TOP', 'VUV'] }; } /** * Get currency information by ISO number * @param {number} number - ISO 4217 currency number * @returns {CurrencyConfig|null} Currency configuration or null if not found */ export function getCurrencyByNumber(number) { const currencyData = currencyCodes.number(number); if (!currencyData) return null; return CURRENCIES[currencyData.code] || null; } /** * Get currencies by country * @param {string} country - Country name (case-insensitive) * @returns {CurrencyConfig[]} Array of currency configurations for the country */ export function getCurrenciesByCountry(country) { const currencies = currencyCodes.country(country); return currencies.map(currency => CURRENCIES[currency.code]).filter(Boolean); } /** * Get the publish date of the ISO 4217 data * @returns {string} Publish date in YYYY-MM-DD format */ export function getISOPublishDate() { return currencyCodes.publishDate; } /** * Get detailed currency information * @param {string} code - ISO currency code * @returns {Object|null} Detailed currency information including countries and number */ export function getCurrencyDetails(code) { const currencyData = currencyCodes.code(code); if (!currencyData) return null; return { ...currencyData, symbol: CURRENCY_SYMBOLS[code] || code, smallestUnit: Math.pow(10, -currencyData.digits) }; }