feat: ( payment ) specific CBE query descriptions + Payment_Reason

This commit is contained in:
Abubeker Yasin
2026-07-31 16:03:28 +03:00
parent 37855b0a83
commit aa4ce7fd23
8 changed files with 183 additions and 38 deletions

View File

@@ -2,30 +2,82 @@ import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";
import { PaymentService } from "@edr/types";
import { PaymentReferenceType, PaymentService } from "@edr/types";
import { PaymentIntent } from "../intents/entities/payment-intent.entity";
import { CbeBillError } from "./mappers/cbe-error.mapper";
/**
* Why a bill is no longer payable. Shared vocabulary between both domain apps and the local
* intent check, so one mapper produces every Response_Description CBE sees.
* `NOT_PAYABLE` is the catch-all for domain states with no better name (booking DRAFT/BOARDED,
* invoice DRAFT) — it must stay last-resort, never a substitute for a specific reason.
*/
export type BillNotPayableReason =
| "ALREADY_PAID"
| "CANCELLED"
| "REFUNDED"
| "EXPIRED"
| "NOT_FOUND"
| "NOT_PAYABLE";
/** Contract of POST /internal/payments/bill-query on the domain apps (plan Phase 4). */
export interface BillQueryResult {
stillPayable: boolean;
payerName?: string | null;
currentAmountMinor?: number | null;
currency?: string | null;
/** When stillPayable=false: "CANCELLED" | "ALREADY_PAID" | "EXPIRED". */
reason?: string | null;
/** When stillPayable=false — see {@link BillNotPayableReason}. */
reason?: BillNotPayableReason | string | null;
/**
* What the payer is paying for, shown on CBE's confirmation screen next to the amount —
* the domain's own human reference (booking ref / invoice number), not our internal ids.
*/
paymentReason?: string | null;
}
const REASON_DESCRIPTIONS: Record<string, string> = {
CANCELLED: "Bill has been cancelled.",
ALREADY_PAID: "Bill already paid.",
EXPIRED: "Bill has expired.",
};
/**
* Payment_Reason when the domain app sends none (older build, or an order with no human
* reference). Generic but never blank: CBE renders this field to the payer, and a bill with
* an amount and no stated purpose is what a customer refuses to confirm.
*/
export function defaultPaymentReason(
referenceType: PaymentReferenceType,
): string {
return referenceType === PaymentReferenceType.BOOKING
? "Train ticket booking"
: "Freight invoice";
}
export function reasonToDescription(reason?: string | null): string {
return (
(reason && REASON_DESCRIPTIONS[reason]) || "Bill is not payable."
);
/**
* CBE reads Response_Description back to the payer at the counter or in the USSD prompt, so it
* has to name the thing they are actually holding — a passenger booking or a freight invoice —
* rather than our internal "bill" abstraction (plan §6.6).
*/
function subjectOf(referenceType: PaymentReferenceType): string {
return referenceType === PaymentReferenceType.BOOKING ? "booking" : "invoice";
}
export function reasonToDescription(
reason: string | null | undefined,
referenceType: PaymentReferenceType,
): string {
const subject = subjectOf(referenceType);
switch (reason) {
case "ALREADY_PAID":
return `This ${subject} has already been paid.`;
case "CANCELLED":
return `This ${subject} has been cancelled.`;
case "REFUNDED":
return `This ${subject} has been refunded.`;
case "EXPIRED":
return `This ${subject} has expired and can no longer be paid.`;
// A bill reference we issued whose order has since vanished from the domain app. Same
// wording as an unknown Bill_Id — from the teller's side it is the same situation.
case "NOT_FOUND":
return "Bill not found.";
default:
return `This ${subject} is no longer payable.`;
}
}
/**