fix(cbe): update response for settled transactions to indicate Already paid

This commit is contained in:
Marshal
2026-08-06 18:18:02 +00:00
parent 9366dd9752
commit 8e75ebf5dc
2 changed files with 12 additions and 26 deletions

View File

@@ -184,24 +184,12 @@ export class CbeBillService {
); );
if (prior) { if (prior) {
if (prior.tradeStatus === "SUCCESS") { if (prior.tradeStatus === "SUCCESS") {
// A replay must be the SAME attempt. A reused id with different money details is // Per CBE integration request: a settled End_To_End_Txn_Id never replays the stored
// not a retry — echoing the stored success would fake a settlement that never ran. // success — every repeat answers "Already paid". Money moved exactly once (the first
const orig = prior.requestPayload as unknown as // call); this only changes what a duplicate hears back. NOTE this diverges from the
| CbePaymentRequestDto // original §6.5 replay design: if CBE retries because our SUCCESS response was lost
| undefined; // in transit, it now sees FAILED for a debit we kept — reconcile such cases manually.
if ( return mapPaymentFailure(dto, "Already paid");
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;
} }
if (prior.tradeStatus === "PENDING") { if (prior.tradeStatus === "PENDING") {
return mapPaymentFailure(dto, "Payment in progress"); return mapPaymentFailure(dto, "Payment in progress");

View File

@@ -129,7 +129,6 @@ describe("CBE Unified Bill (payment service as biller)", () => {
}); });
let settleBody: Record<string, string>; let settleBody: Record<string, string>;
let settledTxnRef: string;
it("settles the freight invoice when CBE reports the debit", async () => { it("settles the freight invoice when CBE reports the debit", async () => {
const invoice = await currentInvoice(invoiceId); 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); const res = await cbe(token, "/cbe/payment", settleBody);
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.Response_Code).toBe("0"); expect(res.body.Response_Code).toBe("0");
settledTxnRef = res.body.Destination_Txn_Ref;
const paid = await poll<{ status: string }>( const paid = await poll<{ status: string }>(
"invoice PAID via CBE bill", "invoice PAID via CBE bill",
@@ -159,22 +157,22 @@ describe("CBE Unified Bill (payment service as biller)", () => {
expect(paid.status).toBe("PAID"); 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); const res = await cbe(token, "/cbe/payment", settleBody);
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.Response_Code).toBe("0"); expect(res.body.Status).toBe("FAILED");
// The stored body, not a re-settlement — same order id as the first answer. expect(res.body.Response_Code).toBe("2");
expect(res.body.Destination_Txn_Ref).toBe(settledTxnRef); 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", { const res = await cbe(token, "/cbe/payment", {
...settleBody, ...settleBody,
Amount: "1.00", Amount: "1.00",
}); });
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.Response_Code).toBe("2"); 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 () => { it("rejects a second debit on the same bill", async () => {