Files
edr-platform/apps/edr-passenger-api/src/modules/currency/currency.service.ts

161 lines
4.6 KiB
TypeScript

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<string, number> = {
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<number> {
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<number> {
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<number> {
if (fromCurrency === toCurrency) {
return amountMinor;
}
const rate = await this.getExchangeRate(fromCurrency, toCurrency);
return Math.round(amountMinor * rate);
}
async getExchangeRate(
fromCurrency: Currency,
toCurrency: Currency,
): Promise<number> {
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<void> {
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;
}
}