Files
edr-platform/apps/edr-payment-api/src/modules/intents/intents.service.cbe-bill.spec.ts
2026-08-05 14:17:00 +00:00

158 lines
5.2 KiB
TypeScript

import { BadRequestException } from "@nestjs/common";
import { DataSource } from "typeorm";
import {
InitiatePaymentRequest,
PaymentReferenceType,
PaymentService,
ProviderMethod,
ProviderPaymentStatus,
} from "@edr/types";
import { CacBankProvider } from "@edr/payment-providers";
import { IntentsService } from "./intents.service";
import { IntentsRepository } from "./intents.repository";
import { BillReferenceService } from "./bill-reference.service";
import { PaymentIntent } from "./entities/payment-intent.entity";
/**
* CBE_BILL regression tests for plan D5 (docs/cbe/CBE_IMPLEMENTATION_PLAN.md): the provider is
* deliberately absent from PAYMENT_PROVIDER_MAP, so the pull-side refresh must return the
* cached intent untouched instead of calling a provider. This is load-bearing — a stub
* provider entry would make the reconciliation sweep expire live CBE bills.
*/
describe("IntentsService CBE_BILL", () => {
const providers = new Map();
let repository: jest.Mocked<
Pick<
IntentsRepository,
| "create"
| "findById"
| "findByIdempotencyKey"
| "findAllByReference"
| "update"
>
>;
let billReferenceService: { generate: jest.Mock };
let service: IntentsService;
const request: InitiatePaymentRequest = {
service: PaymentService.PASSENGER,
referenceType: PaymentReferenceType.BOOKING,
referenceId: "booking-1",
amountMinor: 1500,
currency: "ETB",
provider: ProviderMethod.CBE_BILL,
payerName: "Abebe Kebede",
expiresAt: "2026-08-01T12:00:00.000Z",
};
beforeEach(() => {
repository = {
create: jest.fn(async (data) => ({ id: "intent-1", ...data })),
findById: jest.fn(),
findByIdempotencyKey: jest.fn().mockResolvedValue(null),
findAllByReference: jest.fn().mockResolvedValue([]),
update: jest.fn(),
} as never;
billReferenceService = {
generate: jest.fn().mockResolvedValue("000100000015"),
};
service = new IntentsService(
repository as unknown as IntentsRepository,
{} as DataSource,
providers as never,
{} as CacBankProvider,
billReferenceService as unknown as BillReferenceService,
);
});
it("initiates without a provider session: REQUIRES_ACTION + SHOW_BILL_REFERENCE", async () => {
const snapshot = await service.initiate(request);
expect(snapshot.status).toBe(ProviderPaymentStatus.REQUIRES_ACTION);
expect(snapshot.billReference).toBe("000100000015");
expect(snapshot.clientAction).toMatchObject({
type: "SHOW_BILL_REFERENCE",
billReference: "000100000015",
});
// The booking's own deadline, not a provider-session TTL (plan §6.4).
expect(snapshot.expiresAt).toBe("2026-08-01T12:00:00.000Z");
expect(repository.create).toHaveBeenCalledWith(
expect.objectContaining({
billReference: "000100000015",
payerName: "Abebe Kebede",
}),
);
});
it("reuses the open bill instead of minting a second reference", async () => {
repository.findAllByReference.mockResolvedValue([
{
id: "intent-1",
provider: ProviderMethod.CBE_BILL,
status: ProviderPaymentStatus.REQUIRES_ACTION,
billReference: "000100000015",
amountMinor: 1500,
currency: "ETB",
clientAction: {
type: "SHOW_BILL_REFERENCE",
billReference: "000100000015",
},
},
] as never);
const snapshot = await service.initiate(request);
expect(snapshot.billReference).toBe("000100000015");
expect(billReferenceService.generate).not.toHaveBeenCalled();
expect(repository.create).not.toHaveBeenCalled();
});
it("mints a new bill when the amount changed", async () => {
repository.findAllByReference.mockResolvedValue([
{
id: "intent-1",
provider: ProviderMethod.CBE_BILL,
status: ProviderPaymentStatus.REQUIRES_ACTION,
billReference: "000100000015",
amountMinor: 900,
currency: "ETB",
},
] as never);
billReferenceService.generate.mockResolvedValue("000100000023");
const snapshot = await service.initiate(request);
expect(snapshot.billReference).toBe("000100000023");
});
it("rejects non-ETB currency (plan D8)", async () => {
await expect(
service.initiate({ ...request, currency: "DJF" }),
).rejects.toBeInstanceOf(BadRequestException);
});
it("getIntent leaves a stale CBE_BILL intent untouched (no provider in map — plan D5)", async () => {
const intent = {
id: "intent-1",
service: PaymentService.PASSENGER,
referenceType: PaymentReferenceType.BOOKING,
referenceId: "booking-1",
merchantOrderId: "PSG-x",
provider: ProviderMethod.CBE_BILL,
status: ProviderPaymentStatus.REQUIRES_ACTION,
amountMinor: 1500,
currency: "ETB",
billReference: "000100000015",
// Stale enough that a mapped provider WOULD be queried.
updatedAt: new Date(Date.now() - 60_000),
} as unknown as PaymentIntent;
repository.findById.mockResolvedValue(intent);
const applySpy = jest.spyOn(service, "applyProviderResult");
const snapshot = await service.getIntent("intent-1");
expect(snapshot.status).toBe(ProviderPaymentStatus.REQUIRES_ACTION);
expect(applySpy).not.toHaveBeenCalled();
});
});