fix(cbe): update response code from 3 to 2 for business failures in CBE integration

This commit is contained in:
Marshal
2026-08-06 17:50:27 +00:00
parent 3a2f1a46d6
commit 533de3cc93
8 changed files with 98 additions and 19 deletions

View File

@@ -22,7 +22,7 @@ import { CbePaymentResponseDto } from "./dto/cbe-payment-response.dto";
* CBE Unified Bill Payment — the INBOUND surface CBE core banking calls (docs/cbe/). We are
* the biller: CBE authenticates against /cbe/oauth/token with credentials we issued, then
* presents the bearer token on /cbe/query and /cbe/payment. Business failures answer HTTP 200
* with Response_Code "3"; only authentication answers 401 (plan D6/D7).
* with Response_Code "2"; only authentication answers 401 (plan D6/D7).
*/
@ApiTags("CBE Unified Bill (inbound)")
@Controller("cbe")

View File

@@ -59,7 +59,7 @@ function localReason(intent: PaymentIntent): BillNotPayableReason {
/**
* Orchestration for CBE's three inbound calls (docs/cbe/CBE_IMPLEMENTATION_PLAN.md Phase 3).
* Business failures return HTTP 200 + Response_Code "3" envelopes (never throw past the
* Business failures return HTTP 200 + Response_Code "2" envelopes (never throw past the
* controller); the exception filter only catches auth, validation, and the unexpected.
*/
@Injectable()
@@ -184,6 +184,22 @@ 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,
`End_To_End_Txn_Id ${dto.End_To_End_Txn_Id} was already used by a different payment.`,
);
}
// Replay the stored body verbatim. Never re-settle.
return prior.responsePayload as unknown as CbePaymentResponseDto;
}

View File

@@ -54,7 +54,7 @@ export class CbeExceptionFilter implements ExceptionFilter {
: exception.message;
response.status(HttpStatus.OK).json({
Status: "FAILED",
Response_Code: "3",
Response_Code: "2",
Response_Description: message || "Invalid request",
});
return;
@@ -65,7 +65,7 @@ export class CbeExceptionFilter implements ExceptionFilter {
);
response.status(HttpStatus.OK).json({
Status: "FAILED",
Response_Code: "3",
Response_Code: "2",
Response_Description: "Internal server error.",
});
}

View File

@@ -16,8 +16,8 @@ export class CbeBillError extends Error {
}
/**
* Every failure maps to Response_Code "3" — the AAFDA spec (§2.10, §3.10) defines only
* 0 (success), 1 (auth), 3 (business); only the description is specific (plan §6.6).
* Every failure maps to Response_Code "2" (per current CBE integration requirement; the
* original AAFDA plan used 3); only the description is specific (plan §6.6).
*/
export function toCbeFailure(err: unknown): {
description: string;

View File

@@ -27,7 +27,7 @@ export function mapPaymentFailure(
Cbe_Txn_Ref: request.Cbe_Txn_Ref,
Destination_Txn_Ref: "",
Status: "FAILED",
Response_Code: "3",
Response_Code: "2",
Response_Description: description,
Additional_Fields: [],
};

View File

@@ -48,7 +48,7 @@ export function mapQueryFailure(
Transaction_Type: "",
Timestamp: new Date().toISOString(),
Status: "FAILED",
Response_Code: "3",
Response_Code: "2",
Response_Description: description,
Additional_Fields: [],
};