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

@@ -61,9 +61,13 @@ export class MarkPaidResponseDto {
* "is this order still payable, by whom, for how much" while a CBE teller/app is on the line.
*/
export class BillQueryRequestDto {
// Typed `string`, not the enum: the @nestjs/swagger CLI plugin resolves an enum-typed
// property to a relative require() into packages/types, which does not exist inside the
// Docker image (only /app is copied) and crashes at boot with MODULE_NOT_FOUND. The
// decorators below still give us enum docs + runtime validation.
@ApiProperty({ enum: PaymentReferenceType })
@IsEnum(PaymentReferenceType)
referenceType!: PaymentReferenceType;
referenceType!: string;
@ApiProperty() @IsString() referenceId!: string;
}
@@ -73,6 +77,8 @@ export class BillQueryResponseDto {
@ApiPropertyOptional() payerName?: string | null;
@ApiPropertyOptional() currentAmountMinor?: number | null;
@ApiPropertyOptional() currency?: string | null;
/** When stillPayable=false: "CANCELLED" | "ALREADY_PAID" | "EXPIRED". */
/** When stillPayable=false: "ALREADY_PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED" | "NOT_FOUND" | "NOT_PAYABLE". */
@ApiPropertyOptional() reason?: string | null;
/** What the payer is paying for — CBE renders it beside the amount (Payment_Reason). */
@ApiPropertyOptional() paymentReason?: string | null;
}

View File

@@ -400,7 +400,9 @@ export class PaymentsService {
where: { id: bookingId },
include: { seats: true, passenger: { include: { user: true } } },
});
if (!booking) return { stillPayable: false, reason: "CANCELLED" };
// Distinct from CANCELLED: the payment service issued a bill reference for a booking that
// no longer exists at all, which is a data problem, not a customer-facing cancellation.
if (!booking) return { stillPayable: false, reason: "NOT_FOUND" };
const base = {
// Full_Name is mandatory in CBE's envelope: lead passenger first, then account holder.
@@ -414,14 +416,26 @@ export class PaymentsService {
"ETB",
),
currency: "ETB",
// CBE shows this beside the amount on the confirmation screen. bookingRef is the same
// code on the customer's ticket, so they can match the two before confirming.
paymentReason: `Train ticket booking ${booking.bookingRef}`,
};
// Paid first: a booking that was paid and then boarded/refunded must never be reported as
// merely "not payable" — the payer needs to hear that their money already went through.
if (booking.status === "CONFIRMED" || booking.paidAt) {
return { ...base, stillPayable: false, reason: "ALREADY_PAID" };
}
if (booking.status !== "PENDING_PAYMENT") {
if (booking.status === "REFUNDED") {
return { ...base, stillPayable: false, reason: "REFUNDED" };
}
if (booking.status === "CANCELLED") {
return { ...base, stillPayable: false, reason: "CANCELLED" };
}
// DRAFT / BOARDED / NO_SHOW without a payment: no honest specific wording exists.
if (booking.status !== "PENDING_PAYMENT") {
return { ...base, stillPayable: false, reason: "NOT_PAYABLE" };
}
const deadline = await this.computeBookingPaymentDeadline(booking.id);
if (deadline && deadline.getTime() < Date.now()) {
return { ...base, stillPayable: false, reason: "EXPIRED" };