always-on calendar, currency cards

This commit is contained in:
Marshal
2026-08-05 07:26:41 +00:00
parent 15c43789d3
commit 49c453b82a
3 changed files with 42 additions and 13 deletions

View File

@@ -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',

View File

@@ -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) =>

View File

@@ -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);