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
@@ -585,10 +585,10 @@ export default class PaymentModuleService
status: PaymentSessionStatus,
@MedusaContext() sharedContext?: Context
): Promise<InferEntityType<typeof Payment>> {
let autoCapture = false
let isCaptured = false
if (status === PaymentSessionStatus.CAPTURED) {
status = PaymentSessionStatus.AUTHORIZED
autoCapture = true
isCaptured = true
}
await this.paymentSessionService_.update(
@@ -617,9 +617,13 @@ export default class PaymentModuleService
sharedContext
)
if (autoCapture) {
if (isCaptured) {
await this.capturePayment(
{ payment_id: payment.id, amount: session.amount as BigNumberInput },
{
payment_id: payment.id,
amount: session.amount as BigNumberInput,
is_captured: isCaptured,
},
sharedContext
)
}
@@ -646,8 +650,9 @@ export default class PaymentModuleService
data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PaymentDTO> {
let { is_captured, ...data_ } = data
const payment = await this.paymentService_.retrieve(
data.payment_id,
data_.payment_id,
{
select: [
"id",
@@ -665,8 +670,14 @@ export default class PaymentModuleService
sharedContext
)
let isCaptured = is_captured
if (!isCaptured) {
const isAutoCaptured = !!payment?.captured_at
isCaptured = isAutoCaptured
}
const { isFullyCaptured, capture } = await this.capturePayment_(
data,
data_,
payment,
sharedContext
)
@@ -675,7 +686,7 @@ export default class PaymentModuleService
await this.capturePaymentFromProvider_(
payment,
capture,
isFullyCaptured,
{ isFullyCaptured, isCaptured },
sharedContext
)
} catch (error) {
@@ -763,27 +774,44 @@ export default class PaymentModuleService
protected async capturePaymentFromProvider_(
payment: InferEntityType<typeof Payment>,
capture: InferEntityType<typeof Capture> | undefined,
isFullyCaptured: boolean,
options: {
isFullyCaptured?: boolean
isCaptured?: boolean
} = {},
@MedusaContext() sharedContext: Context = {}
) {
const paymentData = await this.paymentProviderService_.capturePayment(
payment.provider_id,
{
data: payment.data!,
context: {
idempotency_key: capture?.id,
},
}
)
if (!options.isCaptured) {
const paymentData = await this.paymentProviderService_.capturePayment(
payment.provider_id,
{
data: payment.data!,
context: {
idempotency_key: capture?.id,
},
}
)
await this.paymentService_.update(
{
id: payment.id,
data: paymentData.data,
captured_at: isFullyCaptured ? new Date() : undefined,
},
sharedContext
)
await this.paymentService_.update(
{
id: payment.id,
data: paymentData.data,
captured_at: options.isFullyCaptured ? new Date() : undefined,
},
sharedContext
)
} else if (options.isFullyCaptured && !payment.captured_at) {
/**
* In the case of auto capture, we need to update the payment to set the captured_at date
* only if fully captured.
*/
await this.paymentService_.update(
{
id: payment.id,
captured_at: new Date(),
},
sharedContext
)
}
return payment
}