From 8e75ebf5dcda31fcea9a9db58253893a0a124b03 Mon Sep 17 00:00:00 2001 From: Marshal Date: Thu, 6 Aug 2026 18:18:02 +0000 Subject: [PATCH] fix(cbe): update response for settled transactions to indicate Already paid --- .../src/modules/cbe-bill/cbe-bill.service.ts | 24 +++++-------------- integration/src/cbe-bill.it.ts | 14 +++++------ 2 files changed, 12 insertions(+), 26 deletions(-) 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 8a915ece7..1c6926b8a 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 @@ -184,24 +184,12 @@ export class CbeBillService { ); if (prior) { if (prior.tradeStatus === "SUCCESS") { - // A replay must be the SAME attempt. A reused id with different money details is - // not a retry — echoing the stored success would fake a settlement that never ran. - const orig = prior.requestPayload as unknown as - | CbePaymentRequestDto - | undefined; - if ( - orig && - (orig.Bill_Id !== dto.Bill_Id || - orig.Cbe_Txn_Ref !== dto.Cbe_Txn_Ref || - Number(orig.Amount) !== Number(dto.Amount)) - ) { - return mapPaymentFailure( - dto, - "Duplicate End_To_End_Txn_Id", - ); - } - // Replay the stored body verbatim. Never re-settle. - return prior.responsePayload as unknown as CbePaymentResponseDto; + // Per CBE integration request: a settled End_To_End_Txn_Id never replays the stored + // success — every repeat answers "Already paid". Money moved exactly once (the first + // call); this only changes what a duplicate hears back. NOTE this diverges from the + // original §6.5 replay design: if CBE retries because our SUCCESS response was lost + // in transit, it now sees FAILED for a debit we kept — reconcile such cases manually. + return mapPaymentFailure(dto, "Already paid"); } if (prior.tradeStatus === "PENDING") { return mapPaymentFailure(dto, "Payment in progress"); diff --git a/integration/src/cbe-bill.it.ts b/integration/src/cbe-bill.it.ts index f630e9a88..0d1fc7e0e 100644 --- a/integration/src/cbe-bill.it.ts +++ b/integration/src/cbe-bill.it.ts @@ -129,7 +129,6 @@ describe("CBE Unified Bill (payment service as biller)", () => { }); let settleBody: Record; - let settledTxnRef: string; it("settles the freight invoice when CBE reports the debit", async () => { const invoice = await currentInvoice(invoiceId); @@ -147,7 +146,6 @@ describe("CBE Unified Bill (payment service as biller)", () => { const res = await cbe(token, "/cbe/payment", settleBody); expect(res.status).toBe(200); expect(res.body.Response_Code).toBe("0"); - settledTxnRef = res.body.Destination_Txn_Ref; const paid = await poll<{ status: string }>( "invoice PAID via CBE bill", @@ -159,22 +157,22 @@ describe("CBE Unified Bill (payment service as biller)", () => { expect(paid.status).toBe("PAID"); }); - it("replays the stored success when CBE retries the same attempt verbatim", async () => { + it("answers 'Already paid' when the settled attempt is sent again verbatim", async () => { const res = await cbe(token, "/cbe/payment", settleBody); expect(res.status).toBe(200); - expect(res.body.Response_Code).toBe("0"); - // The stored body, not a re-settlement — same order id as the first answer. - expect(res.body.Destination_Txn_Ref).toBe(settledTxnRef); + expect(res.body.Status).toBe("FAILED"); + expect(res.body.Response_Code).toBe("2"); + expect(res.body.Response_Description).toBe("Already paid"); }); - it("rejects a settled End_To_End_Txn_Id reused with a different amount", async () => { + it("answers 'Already paid' when the settled End_To_End_Txn_Id is reused with a different amount", async () => { const res = await cbe(token, "/cbe/payment", { ...settleBody, Amount: "1.00", }); expect(res.status).toBe(200); expect(res.body.Response_Code).toBe("2"); - expect(res.body.Response_Description).toBe("Duplicate End_To_End_Txn_Id"); + expect(res.body.Response_Description).toBe("Already paid"); }); it("rejects a second debit on the same bill", async () => {