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 df81fd5f6..ba6fece8c 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 @@ -279,7 +279,9 @@ export class BookingPricingService { return { lineItems, - totalAmount: total, + // Grand total is billed in whole currency units — fractional line sums + // (rate × tons can yield e.g. 260519.2) round to the nearest whole birr/USD. + totalAmount: Math.round(total), currency: booking.paymentCurrency, usedRates: [...usedRatesMap.values()], appliedModifiers: ruleResult.appliedModifiers, diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx index 8a7949410..ca4ed18ce 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ClearanceDocumentsPage.tsx @@ -161,10 +161,10 @@ export default function ClearanceDocumentsPage() {
-

+

{customer}

-

+

{b.reference}

@@ -182,7 +182,7 @@ export default function ClearanceDocumentsPage() { ) : ( @@ -409,7 +409,7 @@ export default function ClearanceDocumentsPage() { manualPagination: true, pageCount, }} - containerClassName="border-0 shadow-none bg-transparent" + containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words" footer={DataTableFooter} /> diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx index 3d1e20bb6..8719f644a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractClearanceListPage.tsx @@ -393,10 +393,10 @@ function ShipmentBookingsTable({
-

+

{row.original.reference}

-

+

{row.original.customerLabel}

@@ -413,7 +413,7 @@ function ShipmentBookingsTable({ - + {r.contractReference ?? "—"} @@ -431,13 +431,9 @@ function ShipmentBookingsTable({ header: () => Route, cell: ({ row }) => ( - - {row.original.originLabel} - + {row.original.originLabel} - - {row.original.destinationLabel} - + {row.original.destinationLabel} ), }, @@ -610,7 +606,7 @@ function ShipmentBookingsTable({ data={rows} status={loading ? "loading" : error ? "error" : "success"} onRowClick={(row) => onOpen(row.id)} - containerClassName="border-0 shadow-none bg-transparent" + containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words" /> ); diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx index c82cfd152..77ad33f56 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/GlDjiboutiClearanceListPage.tsx @@ -226,11 +226,11 @@ function RouteCell({ return ( - + {origin} - + {destination} @@ -385,10 +385,10 @@ export default function GlDjiboutiClearanceListPage() {
-

+

{r.reference}

-

+

{r.customerLabel}

@@ -403,9 +403,7 @@ export default function GlDjiboutiClearanceListPage() { cell: ({ row }) => ( - - {row.original.contractReference} - + {row.original.contractReference} ), }, @@ -710,7 +708,7 @@ export default function GlDjiboutiClearanceListPage() { manualPagination: true, pageCount, }} - containerClassName="border-0 shadow-none bg-transparent" + containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words" footer={DataTableFooter} /> diff --git a/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.amount.spec.ts b/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.amount.spec.ts new file mode 100644 index 000000000..aabf4f416 --- /dev/null +++ b/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.amount.spec.ts @@ -0,0 +1,22 @@ +import { amountsMatchToTheCent } from "./cbe-bill.service"; + +describe("amountsMatchToTheCent (CBE payment amount gate)", () => { + it("accepts the exact amount", () => { + expect(amountsMatchToTheCent(1234.34, 1234.34)).toBe(true); + }); + + it("rejects a cents-only difference (the 1234.89 vs 1234.34 bug)", () => { + expect(amountsMatchToTheCent(1234.89, 1234.34)).toBe(false); + expect(amountsMatchToTheCent(1234.35, 1234.34)).toBe(false); + }); + + it("rejects whole-unit differences", () => { + expect(amountsMatchToTheCent(1235.34, 1234.34)).toBe(false); + }); + + it("absorbs double-precision storage noise", () => { + expect(amountsMatchToTheCent(1234.34, 1234.3399999999999)).toBe(true); + // classic float artifact: 0.1 + 0.2 !== 0.3 + expect(amountsMatchToTheCent(0.1 + 0.2, 0.3)).toBe(true); + }); +}); diff --git a/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts b/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts index 1c6926b8a..d7f9eb8d6 100644 --- a/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts +++ b/apps/edr-payment-api/src/modules/cbe-bill/cbe-bill.service.ts @@ -38,8 +38,15 @@ import { /** Postgres unique_violation — the DB-level idempotency backstop firing on a concurrent duplicate. */ const PG_UNIQUE_VIOLATION = "23505"; -/** Mirrors the short-pay tolerance already applied in handlePaymentEvent. */ -const AMOUNT_TOLERANCE = 0.01; +/** + * CBE must pay the bill to the exact cent — compare in integer cents so + * double-precision storage noise (1234.34 stored as 1234.33999…) can neither + * mask nor fabricate a difference. A relative tolerance is wrong here: 1% of a + * 1234.34 bill would wave through anything up to ±12.34. + */ +export function amountsMatchToTheCent(a: number, b: number): boolean { + return Math.round(a * 100) === Math.round(b * 100); +} /** * Translate an intent's own terminal state into the same reason vocabulary the domain apps @@ -265,11 +272,15 @@ export class CbeBillService { ); } + // Validate against the freshly-quoted amount — the same figure bill-query + // just showed the payer — not the intent's amount asserted at creation, + // which can go stale when the domain re-prices the invoice. Fallback to + // the intent amount only for domain builds that return no current amount. + const expectedAmount = billQuery.currentAmountMinor ?? intent.amountMinor; const amount = Number(dto.Amount); if ( !Number.isFinite(amount) || - Math.abs(amount - intent.amountMinor) > - intent.amountMinor * AMOUNT_TOLERANCE + !amountsMatchToTheCent(amount, expectedAmount) ) { throw new CbeBillError("Amount mismatch", "BUSINESS"); } diff --git a/docker-compose.yaml b/docker-compose.yaml index 3659e963f..e40d8b49e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,6 +4,7 @@ # # Build: DOCKER_BUILDKIT=1 docker compose build # Run: docker compose up -d + services: freight-api: build: