feat: Add support for idempotency key in payments (#11494)

This commit is contained in:
Stevche Radevski
2025-02-17 18:08:19 +01:00
committed by GitHub
parent 0cbe71597e
commit 91d3332f9e
3 changed files with 87 additions and 29 deletions

View File

@@ -76,6 +76,11 @@ export type PaymentProviderContext = {
* The customer information from Medusa.
*/
customer?: PaymentCustomerDTO
/**
* Idempotency key for the request, if the payment provider supports it. It will be ignored otherwise.
*/
idempotency_key?: string
}
export type PaymentProviderInput = {

View File

@@ -347,7 +347,10 @@ export default class PaymentModuleService
providerPaymentSession = await this.paymentProviderService_.createSession(
input.provider_id,
{
context: input.context,
context: {
idempotency_key: paymentSession!.id,
...input.context,
},
data: { ...input.data, session_id: paymentSession!.id },
amount: input.amount,
currency_code: input.currency_code,
@@ -421,6 +424,7 @@ export default class PaymentModuleService
data: data.data,
amount: data.amount,
currency_code: data.currency_code,
context: data.context,
}
)
@@ -490,7 +494,7 @@ export default class PaymentModuleService
session.provider_id,
{
data: session.data,
context,
context: { idempotency_key: session.id, ...context },
}
)
@@ -515,6 +519,10 @@ export default class PaymentModuleService
} catch (error) {
await this.paymentProviderService_.cancelPayment(session.provider_id, {
data,
context: {
idempotency_key: payment?.id,
...context,
},
})
throw error
@@ -623,6 +631,7 @@ export default class PaymentModuleService
try {
await this.capturePaymentFromProvider_(
payment,
capture,
isFullyCaptured,
sharedContext
)
@@ -703,6 +712,7 @@ export default class PaymentModuleService
@InjectManager()
private async capturePaymentFromProvider_(
payment: InferEntityType<typeof Payment>,
capture: InferEntityType<typeof Capture> | undefined,
isFullyCaptured: boolean,
@MedusaContext() sharedContext: Context = {}
) {
@@ -710,6 +720,9 @@ export default class PaymentModuleService
payment.provider_id,
{
data: payment.data!,
context: {
idempotency_key: capture?.id,
},
}
)
@@ -818,6 +831,9 @@ export default class PaymentModuleService
{
data: payment.data!,
amount: refund.raw_amount as BigNumberInput,
context: {
idempotency_key: refund.id,
},
}
)
@@ -842,6 +858,9 @@ export default class PaymentModuleService
await this.paymentProviderService_.cancelPayment(payment.provider_id, {
data: payment.data!,
context: {
idempotency_key: payment.id,
},
})
await this.paymentService_.update(
@@ -984,7 +1003,12 @@ export default class PaymentModuleService
providerAccountHolder =
await this.paymentProviderService_.createAccountHolder(
input.provider_id,
{ context: input.context }
{
context: {
idempotency_key: input.context?.customer?.id,
...input.context,
},
}
)
// This can be empty when either the method is not supported or an account holder wasn't created

View File

@@ -173,9 +173,9 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
let sessionData
try {
sessionData = (await this.stripe_.paymentIntents.create(
intentRequest
)) as unknown as Record<string, unknown>
sessionData = (await this.stripe_.paymentIntents.create(intentRequest, {
idempotencyKey: context?.idempotency_key,
})) as unknown as Record<string, unknown>
} catch (e) {
throw this.buildError(
"An error occurred in InitiatePayment during the creation of the stripe payment intent",
@@ -198,6 +198,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async cancelPayment({
data,
context,
}: CancelPaymentInput): Promise<CancelPaymentOutput> {
try {
const id = data?.id as string
@@ -206,7 +207,9 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return { data: data }
}
const res = await this.stripe_.paymentIntents.cancel(id)
const res = await this.stripe_.paymentIntents.cancel(id, {
idempotencyKey: context?.idempotency_key,
})
return { data: res as unknown as Record<string, unknown> }
} catch (error) {
if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
@@ -219,11 +222,14 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async capturePayment({
data,
context,
}: CapturePaymentInput): Promise<CapturePaymentOutput> {
const id = data?.id as string
try {
const intent = await this.stripe_.paymentIntents.capture(id)
const intent = await this.stripe_.paymentIntents.capture(id, {
idempotencyKey: context?.idempotency_key,
})
return { data: intent as unknown as Record<string, unknown> }
} catch (error) {
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
@@ -243,6 +249,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async refundPayment({
amount,
data,
context,
}: RefundPaymentInput): Promise<RefundPaymentOutput> {
const id = data?.id as string
if (!id) {
@@ -254,10 +261,15 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
try {
const currencyCode = data?.currency as string
await this.stripe_.refunds.create({
amount: getSmallestUnit(amount, currencyCode),
payment_intent: id as string,
})
await this.stripe_.refunds.create(
{
amount: getSmallestUnit(amount, currencyCode),
payment_intent: id as string,
},
{
idempotencyKey: context?.idempotency_key,
}
)
} catch (e) {
throw this.buildError("An error occurred in refundPayment", e)
}
@@ -284,6 +296,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
data,
currency_code,
amount,
context,
}: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
const amountNumeric = getSmallestUnit(amount, currency_code)
if (isPresent(amount) && data?.amount === amountNumeric) {
@@ -292,9 +305,15 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
try {
const id = data?.id as string
const sessionData = (await this.stripe_.paymentIntents.update(id, {
amount: amountNumeric,
})) as unknown as Record<string, unknown>
const sessionData = (await this.stripe_.paymentIntents.update(
id,
{
amount: amountNumeric,
},
{
idempotencyKey: context?.idempotency_key,
}
)) as unknown as Record<string, unknown>
return { data: sessionData }
} catch (e) {
@@ -305,7 +324,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async createAccountHolder({
context,
}: CreateAccountHolderInput): Promise<CreateAccountHolderOutput> {
const { account_holder, customer } = context
const { account_holder, customer, idempotency_key } = context
if (account_holder?.data?.id) {
return { id: account_holder.data.id as string }
@@ -332,15 +351,20 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
: undefined
try {
const stripeCustomer = await this.stripe_.customers.create({
email: customer.email,
name:
customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone as string | undefined,
...shipping,
})
const stripeCustomer = await this.stripe_.customers.create(
{
email: customer.email,
name:
customer.company_name ||
`${customer.first_name ?? ""} ${customer.last_name ?? ""}`.trim() ||
undefined,
phone: customer.phone as string | undefined,
...shipping,
},
{
idempotencyKey: idempotency_key,
}
)
return {
id: stripeCustomer.id,
@@ -412,10 +436,15 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
)
}
const resp = await this.stripe_.setupIntents.create({
customer: accountHolderId,
...data,
})
const resp = await this.stripe_.setupIntents.create(
{
customer: accountHolderId,
...data,
},
{
idempotencyKey: context?.idempotency_key,
}
)
return { id: resp.id, data: resp as unknown as Record<string, unknown> }
}