fix(payment): Double idempotent capture called with auto capture beha… (#14073)

* fix(payment): Double idempotent capture called with auto capture behaviour

* Create sweet-peaches-dress.md

* naming

* feedback

* naming
This commit is contained in:
Adrien de Peretti
2025-11-18 10:50:50 +01:00
committed by GitHub
parent 6746fecd72
commit 62d103b44f
4 changed files with 187 additions and 26 deletions
@@ -631,6 +631,62 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
})
)
})
it("should auto capture payment when provider returns captured status", async () => {
const collection = await service.createPaymentCollections({
amount: 200,
currency_code: "usd",
})
const session = await service.createPaymentSession(collection.id, {
provider_id: "pp_system_default",
amount: 100,
currency_code: "usd",
data: {},
context: {
customer: { id: "cus-id-1", email: "new@test.tsst" },
},
})
// Mock the provider to return CAPTURED status
const authorizePaymentMock = jest
.spyOn(
(service as any).paymentProviderService_,
"authorizePayment"
)
.mockResolvedValueOnce({
data: { payment_id: "external_payment_id" },
status: "captured",
})
const capturePaymentMock = jest.spyOn(
(service as any).paymentProviderService_,
"capturePayment"
)
const payment = await service.authorizePaymentSession(
session.id,
{}
)
expect(authorizePaymentMock).toHaveBeenCalledTimes(1)
expect(capturePaymentMock).not.toHaveBeenCalled()
expect(payment).toEqual(
expect.objectContaining({
id: expect.any(String),
amount: 100,
currency_code: "usd",
provider_id: "pp_system_default",
captured_at: expect.any(Date),
captures: [expect.objectContaining({})],
payment_session: expect.objectContaining({
status: "authorized",
authorized_at: expect.any(Date),
}),
})
)
})
})
})
@@ -780,6 +836,68 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
"The payment: pay-id-1 has been canceled."
)
})
it("should not call provider capturePayment for auto-captured payments", async () => {
const collection = await service.createPaymentCollections({
amount: 200,
currency_code: "usd",
})
const session = await service.createPaymentSession(collection.id, {
provider_id: "pp_system_default",
amount: 100,
currency_code: "usd",
data: {},
context: {
customer: { id: "cus-id-1", email: "new@test.tsst" },
},
})
// Mock the provider to return CAPTURED status for auto-capture
jest
.spyOn(
(service as any).paymentProviderService_,
"authorizePayment"
)
.mockResolvedValueOnce({
data: { payment_id: "external_payment_id" },
status: "captured",
})
const payment = await service.authorizePaymentSession(
session.id,
{}
)
// Spy on capturePayment provider method
const capturePaymentMock = jest.spyOn(
(service as any).paymentProviderService_,
"capturePayment"
)
// Try to capture the already auto-captured payment
const capturedPayment = await service.capturePayment({
amount: 100,
payment_id: payment.id,
})
// Provider's capturePayment should NOT be called since it was auto-captured
expect(capturePaymentMock).not.toHaveBeenCalled()
// Verify data consistency
expect(capturedPayment).toEqual(
expect.objectContaining({
id: payment.id,
amount: 100,
captured_at: expect.any(Date),
captures: [
expect.objectContaining({
amount: 100,
}),
],
})
)
})
})
describe("refund", () => {