mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix: payemetn race condition
This commit is contained in:
@@ -188,3 +188,45 @@ describe("PaymentClientService.confirmOtp", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PaymentService.markIntentSucceeded", () => {
|
||||
const build = (rows: Record<string, unknown>[]) => {
|
||||
const repo = makeRepo(rows);
|
||||
const billing = { settleByPaymentId: jest.fn().mockResolvedValue(null) };
|
||||
const service = new PaymentService(
|
||||
repo as never,
|
||||
{} as never,
|
||||
billing as never,
|
||||
);
|
||||
return { service, repo, billing };
|
||||
};
|
||||
|
||||
it("re-notifies billing on an already-success intent so a settle that died mid-way converges on redelivery", async () => {
|
||||
const paidAt = new Date("2026-08-01T09:00:00.000Z");
|
||||
const { service, repo, billing } = build([
|
||||
localIntent({ status: "success", transactionId: "txn-1", paidAt }),
|
||||
]);
|
||||
|
||||
const result = await service.markIntentSucceeded("intent-1", {
|
||||
notify: true,
|
||||
});
|
||||
|
||||
expect(result.alreadyFinalized).toBe(true);
|
||||
// No re-write of the intent row…
|
||||
expect(repo.update).not.toHaveBeenCalled();
|
||||
// …but billing still gets the (idempotent) settle call.
|
||||
expect(billing.settleByPaymentId).toHaveBeenCalledWith(
|
||||
"intent-1",
|
||||
"txn-1",
|
||||
paidAt,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not notify billing when notify is false, even when already success", async () => {
|
||||
const { service, billing } = build([localIntent({ status: "success" })]);
|
||||
|
||||
await service.markIntentSucceeded("intent-1", { notify: false });
|
||||
|
||||
expect(billing.settleByPaymentId).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -451,7 +451,19 @@ export class PaymentService {
|
||||
): Promise<{ alreadyFinalized: boolean }> {
|
||||
const intent = await this.paymentRepo.findOneBy({ id: intentId });
|
||||
if (!intent) throw new NotFoundException("PaymentIntent not found");
|
||||
if (intent.status === "success") return { alreadyFinalized: true };
|
||||
if (intent.status === "success") {
|
||||
// Still notify billing: a prior delivery may have flipped the intent to
|
||||
// success and then died before the invoice settled (the two steps are not
|
||||
// atomic). settleByPaymentId is idempotent — no open invoice, no-op.
|
||||
if (opts.notify !== false) {
|
||||
await this.billing.settleByPaymentId(
|
||||
intent.id,
|
||||
opts.providerTxnId ?? intent.transactionId ?? undefined,
|
||||
opts.paidAt ?? intent.paidAt ?? undefined,
|
||||
);
|
||||
}
|
||||
return { alreadyFinalized: true };
|
||||
}
|
||||
|
||||
const paidAt = opts.paidAt ?? new Date();
|
||||
await this.paymentRepo.update(
|
||||
|
||||
Reference in New Issue
Block a user