mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
161 lines
4.6 KiB
TypeScript
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;
|
|
}
|
|
}
|