Payment amount for non ETB and exchange fixes

This commit is contained in:
Stephanos A
2026-07-15 10:27:17 +03:00
parent c0b99b50a9
commit e2529b9317
9 changed files with 216 additions and 125 deletions

View File

@@ -230,7 +230,7 @@ export class PaymentsController {
@ApiOperation({
summary: "Get booking amount in a specific currency",
description:
"Returns the booking total converted from ETB to the requested currency using the latest exchange rate. " +
"Returns the booking total converted from the booking's stored currency to the requested currency using the latest exchange rate. " +
"If currency is ETB the stored amount is returned as-is (no conversion). " +
"Amounts are returned in major currency units (e.g. 162.50 DJF, not centimes).",
})

View File

@@ -38,6 +38,9 @@ describe("PaymentsService", () => {
paymentMethod: {
findUnique: jest.fn(),
},
currencyExchangeRate: {
findFirst: jest.fn(),
},
walletAccount: {
findUnique: jest.fn(),
update: jest.fn(),
@@ -338,6 +341,38 @@ describe("PaymentsService", () => {
});
});
describe("getBookingAmountByCurrency", () => {
it("should convert from the booking currency to the requested currency", async () => {
mockPrisma.booking.findUnique.mockResolvedValue({
id: "booking-1",
totalMinor: 100000,
bookingType: "ONE_WAY",
packageId: null,
priceTierId: null,
currency: "USD",
displayCurrency: "USD",
displayTotalMinor: 125000,
});
mockPrisma.currencyExchangeRate.findFirst.mockResolvedValue({ rate: 2.5 });
const result = await service.getBookingAmountByCurrency("booking-1", "DJF");
expect(result).toEqual({
booking_id: "booking-1",
currency: "DJF",
amount: 3125,
});
expect(mockPrisma.currencyExchangeRate.findFirst).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
fromCurrency: "USD",
toCurrency: "DJF",
}),
}),
);
});
});
describe("getIntentByBookingId", () => {
it("should return the cached local intent when the payment service has none", async () => {
const mockIntent = {

View File

@@ -234,18 +234,36 @@ export class PaymentsService {
);
// The selected method's settlement currency lives in the PaymentMethod table (WAAFI/DMONEY
// settle in DJF, CARD in USD, Ethiopian wallets in ETB). Convert the ETB booking total into
// that currency here so the payment microservice stays currency-agnostic and charges it as-is.
// settle in DJF, CARD in USD, Ethiopian wallets in ETB). When the booking's displayCurrency
// already matches the charge currency, use displayTotalMinor directly — the rate is already
// baked in at booking creation time. Only fall back to ETB→target conversion when they differ.
const paymentMethod = await this.prisma.paymentMethod.findUnique({
where: { type: method },
});
const chargeCurrency = (
paymentMethod?.currency ?? booking.currency
).toUpperCase();
const chargeAmount = await this.currencyService.convertEtbMinorToChargeMajor(
booking.totalMinor,
chargeCurrency,
);
const bookingDisplayCurrency = ((booking as any).displayCurrency ?? 'ETB').toUpperCase();
const bookingDisplayTotalMinor = (booking as any).displayTotalMinor as number | null;
let chargeAmount: number;
if (
chargeCurrency === bookingDisplayCurrency &&
chargeCurrency !== 'ETB' &&
bookingDisplayTotalMinor != null
) {
// Display currency matches charge currency — use the pre-converted amount directly.
chargeAmount = this.currencyService.displayMinorToChargeMajor(bookingDisplayTotalMinor, chargeCurrency);
} else if (chargeCurrency === 'ETB') {
chargeAmount = this.currencyService.displayMinorToChargeMajor(booking.totalMinor, 'ETB');
} else {
// Booking is in ETB — convert to the provider's settlement currency.
chargeAmount = await this.currencyService.convertEtbMinorToChargeMajor(
booking.totalMinor,
chargeCurrency,
);
}
const snapshot = await this.paymentClient.initiate({
service: PaymentServiceEnum.PASSENGER,
@@ -657,28 +675,54 @@ export class PaymentsService {
): Promise<{ booking_id: string; currency: string; amount: number }> {
const booking = await this.prisma.booking.findUnique({
where: { id: bookingId },
select: { id: true, totalMinor: true, bookingType: true, packageId: true, priceTierId: true },
select: {
id: true,
totalMinor: true,
bookingType: true,
packageId: true,
priceTierId: true,
currency: true,
displayCurrency: true,
displayTotalMinor: true,
},
});
if (!booking) throw new NotFoundException('Booking not found');
const correctTotalMinor = await this.resolveBookingTotal(booking as any);
const requestedCurrency = currency.toUpperCase();
const amountInETB = correctTotalMinor / 100;
if (requestedCurrency === 'ETB') {
return { booking_id: bookingId, currency: 'ETB', amount: amountInETB };
// Source of truth: displayTotalMinor in displayCurrency when available,
// otherwise totalMinor in ETB (bookings with no display currency override).
const sourceCurrency = (booking.displayCurrency ?? 'ETB').toUpperCase();
const sourceMinor = booking.displayTotalMinor ?? correctTotalMinor;
// Same currency — return directly, no conversion needed.
if (requestedCurrency === sourceCurrency) {
return { booking_id: bookingId, currency: requestedCurrency, amount: sourceMinor / 100 };
}
const exchangeRate = await this.prisma.currencyExchangeRate.findFirst({
where: { fromCurrency: 'ETB' as any, toCurrency: requestedCurrency as any },
where: { fromCurrency: sourceCurrency as any, toCurrency: requestedCurrency as any },
orderBy: { effectiveDate: 'desc' },
});
if (!exchangeRate) {
throw new NotFoundException(`Exchange rate not found for ETB → ${requestedCurrency}`);
}
const rate = Number(exchangeRate.rate);
const converted = parseFloat((amountInETB * rate).toFixed(2));
let rate: number;
if (exchangeRate) {
rate = Number(exchangeRate.rate);
} else {
// Try inverse rate
const inverseRate = await this.prisma.currencyExchangeRate.findFirst({
where: { fromCurrency: requestedCurrency as any, toCurrency: sourceCurrency as any },
orderBy: { effectiveDate: 'desc' },
});
if (inverseRate) {
rate = 1 / Number(inverseRate.rate);
} else {
// Bridge via ETB (e.g. DJF→USD = (DJF→ETB) × (ETB→USD))
rate = await this.currencyService.getRateOrThrow(sourceCurrency as any, requestedCurrency as any);
}
}
const converted = (sourceMinor / 100) * rate;
return { booking_id: bookingId, currency: requestedCurrency, amount: converted };
}