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: