diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts index 4996f45a9..75a179b5c 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.spec.ts @@ -87,6 +87,33 @@ describe('BookingPricingService — domestic corridor', () => { expect(result.lineItems[0].amount).toBe(Math.round(35 * 120 * MOCK_CBE_RATE)); }); + it('keeps the exchange rate decimals — ETB amounts round to cents, not whole birr', async () => { + exchangeService.getRate.mockResolvedValue(162.2132); + const booking = { + id: 'b-1-frac', + freightType: 'BULK', + tradeDirection: 'DOMESTIC', + paymentCurrency: 'ETB', + cargoTotalWeightVgm: 120, + originYardId: MOJO, + destinationYardId: DIRE, + bookingContainers: [], + } as unknown as Booking; + + const result = await ( + service as unknown as { + computeBaseRailLinesWithRates: ( + b: Booking, + input: { containers: [] }, + ) => Promise<{ lineItems: Array<{ amount: number }> }>; + } + ).computeBaseRailLinesWithRates(booking, { containers: [] }); + + // 35 × 120 × 162.2132 = 681,295.44 — the .44 must survive (whole-birr + // rounding here billed with the integer part of the rate, in effect). + expect(result.lineItems[0].amount).toBe(681295.44); + }); + it('prices domestic bulk in USD using INTERCITY_BULK USD rate directly', async () => { const booking = { id: 'b-1-usd', diff --git a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts index 9bbe6bed5..522bac47e 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-pricing.service.ts @@ -6,6 +6,7 @@ import { RatesService } from '../rule-engine/services/rates.service'; import { Rate } from '../rule-engine/entities/rate.entity'; import { isBulkQuantityUnit } from '../rule-engine/entities/rate-unit.util'; import { ContractRateSnapshot } from '../contracts/entities/contract-rate-snapshot.entity'; +import { round2 } from '../billing/invoice-settlement.util'; import { ExchangeService } from '@edr/api-common'; import { AppliedCargoModifier, @@ -205,14 +206,14 @@ export class BookingPricingService { const unitAmount = frozen ? Number(frozen.unitPrice) : isEtbBooking - ? Math.round(unitUsd * usdToEtb) + ? round2(unitUsd * usdToEtb) : unitUsd; const convertedAmount = frozen ? isEtbBooking - ? Math.round(unitAmount * quantity) + ? round2(unitAmount * quantity) : unitAmount * quantity : isEtbBooking - ? Math.round(usdAmount * usdToEtb) + ? round2(usdAmount * usdToEtb) : usdAmount; const item: PriceLineItemDto = { @@ -583,8 +584,8 @@ export class BookingPricingService { } else { const unitUsd = Number(rate!.rateValue); const usdAmount = this.amountForRate(rate!, container.quantity, lineWagons); - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd; } if (rate) usedRatesMap.set(rate.id, rate); lines.push({ @@ -652,8 +653,8 @@ export class BookingPricingService { ); } else { const usdAmount = this.amountForRate(fallback, quantity, wagonCount); - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd; } lines.push({ code: rateType, @@ -763,12 +764,12 @@ export class BookingPricingService { if (frozen) { unitAmount = Number(frozen.unitPrice); amount = isEtbBooking - ? Math.round(unitAmount * quantity) + ? round2(unitAmount * quantity) : unitAmount * quantity; } else { const usdAmount = value * quantity; - amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount; - unitAmount = isEtbBooking ? Math.round(value * usdToEtb) : value; + amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount; + unitAmount = isEtbBooking ? round2(value * usdToEtb) : value; } // Skip legs that resolve to nothing (zero rate, or zero km / count / tons). if (!(amount > 0)) continue; @@ -971,7 +972,7 @@ export class BookingPricingService { if (!(usdToEtb > 0)) return null; const converted = snap.currency === 'USD' && bookingCurrency === 'ETB' - ? Math.round(unitPrice * usdToEtb) + ? round2(unitPrice * usdToEtb) : snap.currency === 'ETB' && bookingCurrency === 'USD' ? unitPrice / usdToEtb : null; @@ -1027,7 +1028,7 @@ export class BookingPricingService { const currency = booking.paymentCurrency; const isEtb = currency === 'ETB'; const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1; - const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd); + const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd); const onLeg = liveRates.filter( (r) => diff --git a/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts index d54b0b544..3f395c369 100644 --- a/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contract-pricing.service.ts @@ -2,6 +2,7 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { RatesService } from '../rule-engine/services/rates.service'; import { ContainerTypesService } from '../rule-engine/services/container-types.service'; +import { round2 } from '../billing/invoice-settlement.util'; import { ExchangeService } from '@edr/api-common'; import { ContractsRepository } from './contracts.repository'; import { Contract } from './entities/contract.entity'; @@ -79,7 +80,7 @@ export class ContractPricingService { const currency = contract.paymentCurrency; const isEtb = currency === 'ETB'; const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1; - const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd); + const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd); const lineItems: ContractUnitRateLineItem[] = []; const baseType = this.baseRateType(contract);