mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
keep cents in CBE bills and prices
This commit is contained in:
@@ -677,11 +677,11 @@ describe("BillingService — CAC Bank (OTP debit)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("BillingService — CBE bill amounts round UP to whole birr", () => {
|
||||
// CBE bills whole birr. Ceil, never Math.round: a .40 balance rounded down
|
||||
// settles 0.40 short while markInvoiceAsPaid still writes paidAmount =
|
||||
// totalAmount — money missing from the bank with the books saying paid.
|
||||
// payInvoice and billQuery must agree, or /cbe/payment sees a mismatch.
|
||||
describe("BillingService — CBE bill amounts carry cents, never rounded", () => {
|
||||
// CBE settles to the cent (/cbe/payment gates on amountsMatchToTheCent), so the
|
||||
// bill must quote the exact balance. Rounding UP overcharged the payer by up to
|
||||
// a birr; rounding DOWN underpaid while markInvoiceAsPaid still wrote paidAmount
|
||||
// = totalAmount. payInvoice and billQuery must agree, or /cbe/payment mismatches.
|
||||
const invoice = {
|
||||
id: "inv-1",
|
||||
status: Freight.InvoiceStatus.Pending,
|
||||
@@ -690,9 +690,9 @@ describe("BillingService — CBE bill amounts round UP to whole birr", () => {
|
||||
type: "PREPAID",
|
||||
invoiceNumber: "INV-20260101-00001",
|
||||
currency: "ETB",
|
||||
// .40 — the case Math.round gets wrong (rounds down, underpays).
|
||||
balanceAmount: 12345.4,
|
||||
totalAmount: 12345.4,
|
||||
// .43 — cents that must survive all the way to the bill.
|
||||
balanceAmount: 12345.43,
|
||||
totalAmount: 12345.43,
|
||||
company: { name: "Acme PLC" },
|
||||
paymentId: null,
|
||||
dueAt: null,
|
||||
@@ -716,7 +716,7 @@ describe("BillingService — CBE bill amounts round UP to whole birr", () => {
|
||||
return { service, repo };
|
||||
};
|
||||
|
||||
it("opens the intent for the ceiled balance, never below it", async () => {
|
||||
it("opens the intent for the exact balance, cents included", async () => {
|
||||
const initiate = jest.fn().mockResolvedValue({
|
||||
intentId: "intent-1",
|
||||
immediateSuccess: false,
|
||||
@@ -727,16 +727,16 @@ describe("BillingService — CBE bill amounts round UP to whole birr", () => {
|
||||
await service.payInvoice("inv-1", { method: "CBE_BILL" });
|
||||
|
||||
expect(initiate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ amountMinor: 12346 }),
|
||||
expect.objectContaining({ amountMinor: 12345.43 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("quotes the same ceiled amount on bill-query as payInvoice opened", async () => {
|
||||
it("quotes the same exact amount on bill-query as payInvoice opened", async () => {
|
||||
const { service } = build();
|
||||
|
||||
await expect(service.billQuery("booking-1")).resolves.toMatchObject({
|
||||
stillPayable: true,
|
||||
currentAmountMinor: 12346,
|
||||
currentAmountMinor: 12345.43,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1341,11 +1341,11 @@ export class BillingService {
|
||||
// service branches on a domain-specific reference type.
|
||||
referenceType: PaymentReferenceType.SHIPMENT,
|
||||
orderRef: invoice.invoiceNumber.replace(/-/g, "_"),
|
||||
// Whole birr, always UP. CBE bills this amount verbatim, so it must never
|
||||
// land below the outstanding balance — Math.round would let a .40 balance
|
||||
// settle 0.40 short. Ceil overcharges by <1 birr instead, and the same
|
||||
// ceil in billQuery keeps the quoted and debited amounts identical.
|
||||
amountMinor: Math.ceil(Number(invoice.balanceAmount)),
|
||||
// Exact balance, cents included. CBE bills this verbatim and /cbe/payment
|
||||
// matches the debited amount to the cent (amountsMatchToTheCent), so any
|
||||
// rounding here would overcharge the payer and leave the invoice balance
|
||||
// non-zero. billQuery quotes the same unrounded value.
|
||||
amountMinor: round2(Number(invoice.balanceAmount)),
|
||||
currency: invoice.currency,
|
||||
reason: `Payment for invoice ${invoice.invoiceNumber}`,
|
||||
method: opts.method ?? "TELEBIRR",
|
||||
@@ -1490,9 +1490,10 @@ export class BillingService {
|
||||
});
|
||||
|
||||
if (open) {
|
||||
// Ceil, matching payInvoice — the amount CBE quotes at the counter has to
|
||||
// be the amount the intent was opened for, or /cbe/payment sees a mismatch.
|
||||
const balance = Math.ceil(Number(open.balanceAmount ?? open.totalAmount));
|
||||
// Unrounded, matching payInvoice — the amount CBE quotes at the counter has
|
||||
// to be the amount the intent was opened for, to the cent, or /cbe/payment
|
||||
// sees a mismatch.
|
||||
const balance = round2(Number(open.balanceAmount ?? open.totalAmount));
|
||||
const expired = open.dueAt && open.dueAt.getTime() < Date.now();
|
||||
return {
|
||||
stillPayable: balance > 0 && !expired,
|
||||
@@ -1527,7 +1528,7 @@ export class BillingService {
|
||||
return {
|
||||
stillPayable: false,
|
||||
payerName: latest.company?.name ?? null,
|
||||
currentAmountMinor: Math.ceil(Number(latest.totalAmount)),
|
||||
currentAmountMinor: round2(Number(latest.totalAmount)),
|
||||
currency: latest.currency,
|
||||
paymentReason: `Freight invoice ${latest.invoiceNumber}`,
|
||||
reason: closedInvoiceReason(latest.status),
|
||||
|
||||
Reference in New Issue
Block a user