mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
158 lines
5.2 KiB
TypeScript
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();
|
|
});
|
|
});
|