import { ok, err, type ReadonlyDB, type CallerContext } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { CurrencyNotFoundError, ExchangeRateNotFoundError, InactiveCurrencyError, } from "../lib/errors.generated"; import { getCurrency } from "./getCurrency.generated"; export interface ConvertAmountInput { amount: number; sourceCurrencyCode: string; targetCurrencyCode: string; conversionDate: string; } /** * Converts a monetary amount from one currency to another using the applicable * exchange rate for a given date. Currencies are identified by their ISO 4217 * code (e.g., "USD", "EUR", "JPY"). The function looks up the most recent exchange * rate on or before the specified date. If no direct rate exists, it calculates * the inverse rate. Result is rounded to the target currency's decimal precision. */ export async function run(db: ReadonlyDB, input: ConvertAmountInput, ctx: CallerContext) { // Validate source currency exists const { currency: sourceCurrency } = ( await getCurrency( db, { code: input.sourceCurrencyCode, }, ctx, ) ).value; if (!sourceCurrency) { return err(new CurrencyNotFoundError(input.sourceCurrencyCode)); } // Validate target currency exists const { currency: targetCurrency } = ( await getCurrency( db, { code: input.targetCurrencyCode, }, ctx, ) ).value; if (!targetCurrency) { return err(new CurrencyNotFoundError(input.targetCurrencyCode)); } // Validate both currencies are active if (sourceCurrency.status !== "ACTIVE") { return err(new InactiveCurrencyError(input.sourceCurrencyCode)); } if (targetCurrency.status !== "ACTIVE") { return err(new InactiveCurrencyError(input.targetCurrencyCode)); } // Same currency - return original amount if (sourceCurrency.id === targetCurrency.id) { return ok({ convertedAmount: input.amount, exchangeRate: 1, sourceCurrency, targetCurrency, exchangeRateRecord: null, }); } // Find direct exchange rate (most recent on or before conversion date) const conversionDate = new Date(input.conversionDate); const directRate = await db .selectFrom("ExchangeRate") .selectAll() .where("sourceCurrencyId", "=", sourceCurrency.id) .where("targetCurrencyId", "=", targetCurrency.id) .where("effectiveDate", "<=", conversionDate) .orderBy("effectiveDate", "desc") .executeTakeFirst(); if (directRate) { const exchangeRate = directRate.rate; const exchangeRateRecord = directRate; const rawResult = input.amount * exchangeRate; const roundingFactor = Math.pow(10, targetCurrency.decimalPlaces); const convertedAmount = Math.round(rawResult * roundingFactor) / roundingFactor; return ok({ convertedAmount, exchangeRate, sourceCurrency, targetCurrency, exchangeRateRecord, }); } // Try inverse rate const inverseRate = await db .selectFrom("ExchangeRate") .selectAll() .where("sourceCurrencyId", "=", targetCurrency.id) .where("targetCurrencyId", "=", sourceCurrency.id) .where("effectiveDate", "<=", conversionDate) .orderBy("effectiveDate", "desc") .executeTakeFirst(); if (!inverseRate) { return err( new ExchangeRateNotFoundError( `${input.sourceCurrencyCode} to ${input.targetCurrencyCode} on ${input.conversionDate}`, ), ); } const exchangeRate = 1 / inverseRate.rate; const exchangeRateRecord = inverseRate; const rawResult = input.amount * exchangeRate; const roundingFactor = Math.pow(10, targetCurrency.decimalPlaces); const convertedAmount = Math.round(rawResult * roundingFactor) / roundingFactor; return ok({ convertedAmount, exchangeRate, sourceCurrency, targetCurrency, exchangeRateRecord, }); }