/** * 多币种支持(plan.md「5.8 多币种支持」)。 * 参考 BeeCount v30 的交易级多币种(nativeAmount + 汇率快照)。 */ import { compareDecimals, divideDecimals, multiplyDecimals, parseDecimal } from './decimal'; export interface ExchangeRate { from: string; to: string; rate: string; date: string; } /** 手动汇率覆盖(用户优先)。 */ export interface ExchangeRateOverride { from: string; to: string; rate: string; enabled: boolean; } /** 交易级多币种快照。 */ export interface TransactionCurrency { currencyCode: string; // 交易货币(如 USD) nativeAmount: string; // 原始金额 exchangeRate: string; // 记账时的汇率快照 baseCurrency: string; // 基础货币(如 CNY) } /** 货币转换。 */ export function convertCurrency( amount: string, from: string, to: string, rates: ExchangeRate[], ): string { if (from === to) return amount; const rate = rates.find(r => r.from === from && r.to === to); if (!rate) throw new Error(`无 ${from} → ${to} 的汇率`); const result = multiplyDecimals(amount, rate.rate); // 货币结果保留两位小数(使用 string decimal 精确截断,避免 parseFloat 精度风险) const parsed = parseDecimal(result); // 乘以 100 取整再除以 100,等效于截断到 2 位小数 const scaled = (parsed.coefficient * 100n) / (10n ** BigInt(parsed.scale)); // 手动格式化,保留尾随零(货币显示惯例:720 → 720.00) const sign = scaled < 0n ? '-' : ''; const abs = scaled < 0n ? -scaled : scaled; const intPart = abs / 100n; const fracPart = abs % 100n; return `${sign}${intPart}.${fracPart.toString().padStart(2, '0')}`; } /** * 获取有效汇率(优先手动覆盖)。 */ export function getEffectiveRate( from: string, to: string, autoRates: ExchangeRate[], overrides: ExchangeRateOverride[], ): ExchangeRate | null { // 手动覆盖优先 const override = overrides.find(o => o.enabled && o.from === from && o.to === to); if (override) { return { from, to, rate: override.rate, date: new Date().toISOString().slice(0, 10) }; } return autoRates.find(r => r.from === from && r.to === to) ?? null; } /** 构造交易级多币种快照(记账时调用)。 */ export function buildTransactionCurrency( nativeAmount: string, currencyCode: string, baseCurrency: string, rates: ExchangeRate[], ): TransactionCurrency { if (currencyCode === baseCurrency) { return { currencyCode, nativeAmount, exchangeRate: '1', baseCurrency }; } const rate = rates.find(r => r.from === currencyCode && r.to === baseCurrency) ?? rates.find(r => r.from === baseCurrency && r.to === currencyCode); if (!rate) throw new Error(`无 ${currencyCode}/${baseCurrency} 汇率`); // 正向汇率直接用,反向取倒数 const effectiveRate = rate.from === currencyCode ? rate.rate : divideDecimals('1', rate.rate); return { currencyCode, nativeAmount, exchangeRate: effectiveRate, baseCurrency }; } /** 校验汇率列表完整性(基础货币对所有其他货币都有汇率)。 */ export function validateRateCoverage( baseCurrency: string, currencies: string[], rates: ExchangeRate[], ): string[] { const missing: string[] = []; for (const cur of currencies) { if (cur === baseCurrency) continue; const has = rates.some(r => (r.from === cur && r.to === baseCurrency) || (r.from === baseCurrency && r.to === cur) ); if (!has) missing.push(cur); } return missing; }