diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index c42f595be8..8390348371 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -504,6 +504,11 @@ export default class PaymentModuleService status !== PaymentSessionStatus.AUTHORIZED && status !== PaymentSessionStatus.CAPTURED ) { + await this.paymentSessionService_.update({ + id: session.id, + status, + data, + }, sharedContext); throw new MedusaError( MedusaError.Types.NOT_ALLOWED, `Session: ${session.id} was not authorized with the provider.` diff --git a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts index d079cf0f33..11e239c303 100644 --- a/packages/modules/providers/payment-stripe/src/core/stripe-base.ts +++ b/packages/modules/providers/payment-stripe/src/core/stripe-base.ts @@ -1,5 +1,5 @@ import Stripe from "stripe" - +import { setTimeout } from "timers/promises" import { AuthorizePaymentInput, AuthorizePaymentOutput, @@ -50,6 +50,14 @@ import { getSmallestUnit, } from "../utils/get-smallest-unit" +type StripeIndeterminateState = { + indeterminate_due_to: string, +} +type StripeErrorData = Stripe.PaymentIntent | StripeIndeterminateState +type HandledErrorType = + | { retry: true; } + | { retry: false; data: StripeErrorData }; + abstract class StripeBase extends AbstractPaymentProvider { protected readonly options_: StripeOptions protected stripe_: Stripe @@ -115,6 +123,87 @@ abstract class StripeBase extends AbstractPaymentProvider { return res } + handleStripeError(error: any): HandledErrorType { + switch (error.type) { + case 'StripeCardError': + // Stripe has created a payment intent but it failed + // Extract and return paymentIntent object to be stored in payment_session + // Allows for reference to the failed intent and potential webhook reconciliation + const stripeError = error.raw as Stripe.errors.StripeCardError + if (stripeError.payment_intent) { + return { + retry: false, + data: stripeError.payment_intent + } + } else { + throw this.buildError( + "An error occurred in InitiatePayment during creation of stripe payment intent", + error + ) + } + + case 'StripeConnectionError': + case 'StripeRateLimitError': + // Connection or rate limit errors indicate an uncertain result + // Retry the operation + return { + retry: true, + } + case 'StripeAPIError': { + // API errors should be treated as indeterminate per Stripe documentation + // Rely on webhooks rather than assuming failure + return { + retry: false, + data: { + indeterminate_due_to: "stripe_api_error" + } + } + } + default: + // For all other errors, there was likely an issue creating the session + // on Stripe's servers. Throw an error which will trigger cleanup + // and deletion of the payment session. + throw this.buildError( + "An error occurred in InitiatePayment during creation of stripe payment intent", + error + ) + } + } + + async executeWithRetry( + apiCall: () => Promise, + maxRetries: number = 3, + baseDelay: number = 1000, + currentAttempt: number = 1 + ): Promise { + try { + return await apiCall(); + } catch (error) { + const handledError = this.handleStripeError(error) + + if (!handledError.retry) { + // If retry is false, we know data exists per the type definition + return handledError.data; + } + + if ( + handledError.retry && + currentAttempt <= maxRetries + ) { + // Logic for retrying + const delay = baseDelay * Math.pow(2, currentAttempt - 1) * (0.5 + Math.random() * 0.5); + await setTimeout(delay); + return this.executeWithRetry(apiCall, maxRetries, baseDelay, currentAttempt + 1); + } + // Retries are exhausted + throw this.buildError( + "An error occurred in InitiatePayment during creation of stripe payment intent", + error + ) + } + } + + async getPaymentStatus({ data, }: GetPaymentStatusInput): Promise { @@ -173,21 +262,15 @@ abstract class StripeBase extends AbstractPaymentProvider { | string | undefined - let sessionData - try { - sessionData = (await this.stripe_.paymentIntents.create(intentRequest, { + const sessionData = await this.executeWithRetry( + () => this.stripe_.paymentIntents.create(intentRequest, { idempotencyKey: context?.idempotency_key, - })) as unknown as Record - } catch (e) { - throw this.buildError( - "An error occurred in InitiatePayment during the creation of the stripe payment intent", - e - ) - } - + }) + ) + const isPaymentIntent = 'id' in sessionData; return { - id: sessionData.id, - data: sessionData, + id: isPaymentIntent ? sessionData.id : (data?.session_id as string), + data: sessionData as unknown as Record, } }