import { Injectable, Logger, NotFoundException, BadRequestException, } from '@nestjs/common'; import { PrismaService } from '../../common/prisma.service'; import { Currency } from '@prisma/client'; /** * Minor-unit decimal places per currency, used to round the CHARGE amount sent to the payment * microservice. DJF has no minor unit (whole francs only); ETB and USD use 2 decimals. */ const CHARGE_CURRENCY_DECIMALS: Record = { ETB: 2, USD: 2, DJF: 0, }; @Injectable() export class CurrencyService { private readonly logger = new Logger(CurrencyService.name); constructor(private readonly prisma: PrismaService) {} async convertEtbMinorToChargeMajor( amountMinorEtb: number, targetCurrency: string, ): Promise { const target = targetCurrency.toUpperCase(); const decimals = CHARGE_CURRENCY_DECIMALS[target]; if (decimals === undefined) { throw new BadRequestException(`Unsupported charge currency: ${targetCurrency}`); } const sourceMajor = amountMinorEtb / 100; if (target === Currency.ETB) { return this.roundTo(sourceMajor, decimals); } const rate = await this.getRateOrThrow(Currency.ETB, target as Currency); return this.roundTo(sourceMajor * rate, decimals); } async getRateOrThrow( fromCurrency: Currency, toCurrency: Currency, ): Promise { if (fromCurrency === toCurrency) return 1; const exchangeRate = await this.prisma.currencyExchangeRate.findFirst({ where: { fromCurrency, toCurrency }, orderBy: { effectiveDate: 'desc' }, }); if (!exchangeRate) { throw new BadRequestException( `No exchange rate configured for ${fromCurrency}->${toCurrency}`, ); } return Number(exchangeRate.rate); } private roundTo(value: number, decimals: number): number { const factor = 10 ** decimals; return Math.round(value * factor) / factor; } async convertAmount( amountMinor: number, fromCurrency: Currency, toCurrency: Currency, ): Promise { if (fromCurrency === toCurrency) { return amountMinor; } const rate = await this.getExchangeRate(fromCurrency, toCurrency); return Math.round(amountMinor * rate); } async getExchangeRate( fromCurrency: Currency, toCurrency: Currency, ): Promise { const exchangeRate = await this.prisma.currencyExchangeRate.findFirst({ where: { fromCurrency, toCurrency, }, orderBy: { effectiveDate: 'desc', }, }); if (!exchangeRate) { this.logger.warn( `No exchange rate found for ${fromCurrency} to ${toCurrency}, using 1.0`, ); return 1.0; } return Number(exchangeRate.rate); } async syncExchangeRates(): Promise { this.logger.log('Syncing exchange rates from external provider'); const today = this.todayUtc(); const rates = [ { from: 'ETB', to: 'ETB', rate: 1.0 }, { from: 'ETB', to: 'DJF', rate: 3.25 }, { from: 'ETB', to: 'USD', rate: 0.018 }, { from: 'DJF', to: 'ETB', rate: 0.3077 }, { from: 'USD', to: 'ETB', rate: 55.56 }, ]; for (const { from, to, rate } of rates) { await this.upsertRate(from as Currency, to as Currency, rate, today, 'EXTERNAL_API'); } this.logger.log('Exchange rates synced successfully'); } async listRates() { return this.prisma.currencyExchangeRate.findMany({ orderBy: [{ fromCurrency: 'asc' }, { toCurrency: 'asc' }, { effectiveDate: 'desc' }], }); } async upsertRate( fromCurrency: Currency, toCurrency: Currency, rate: number, effectiveDate?: Date, source = 'MANUAL', ) { const date = effectiveDate ?? this.todayUtc(); return this.prisma.currencyExchangeRate.upsert({ where: { fromCurrency_toCurrency_effectiveDate: { fromCurrency, toCurrency, effectiveDate: date } }, update: { rate, source }, create: { fromCurrency, toCurrency, rate, effectiveDate: date, source }, }); } async updateRateById(id: string, rate: number, source = 'MANUAL') { const existing = await this.prisma.currencyExchangeRate.findUnique({ where: { id } }); if (!existing) throw new NotFoundException('Exchange rate not found'); return this.prisma.currencyExchangeRate.update({ where: { id }, data: { rate, source } }); } async deleteRate(id: string) { return this.prisma.currencyExchangeRate.delete({ where: { id } }); } /** Returns midnight UTC for today — used as the date-only key for upserts. */ private todayUtc(): Date { const d = new Date(); d.setUTCHours(0, 0, 0, 0); return d; } }