feat: Normalize payment method data and options when passed to Stripe (#12757)

This commit is contained in:
Stevche Radevski authored and GitHub committed 2025-06-18 09:23:42 +02:00
1 parent 9bdd5429af
commit 5856963e0b
1 file changed
+36 -25
@@ -51,12 +51,12 @@ import {
} from "../utils/get-smallest-unit" } from "../utils/get-smallest-unit"
type StripeIndeterminateState = { type StripeIndeterminateState = {
indeterminate_due_to: string, indeterminate_due_to: string
} }
type StripeErrorData = Stripe.PaymentIntent | StripeIndeterminateState type StripeErrorData = Stripe.PaymentIntent | StripeIndeterminateState
type HandledErrorType = type HandledErrorType =
| { retry: true; } | { retry: true }
| { retry: false; data: StripeErrorData }; | { retry: false; data: StripeErrorData }
abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> { abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
protected readonly options_: StripeOptions protected readonly options_: StripeOptions
@@ -105,8 +105,15 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
(extra?.setup_future_usage as "off_session" | "on_session" | undefined) ?? (extra?.setup_future_usage as "off_session" | "on_session" | undefined) ??
this.paymentIntentOptions.setup_future_usage this.paymentIntentOptions.setup_future_usage
res.payment_method_types = this.paymentIntentOptions res.payment_method_types =
.payment_method_types as string[] (extra?.payment_method_types as string[]) ??
(this.paymentIntentOptions.payment_method_types as string[])
res.payment_method_data =
extra?.payment_method_data as Stripe.PaymentIntentCreateParams.PaymentMethodData
res.payment_method_options =
extra?.payment_method_options as Stripe.PaymentIntentCreateParams.PaymentMethodOptions
res.automatic_payment_methods = res.automatic_payment_methods =
(extra?.automatic_payment_methods as { enabled: true } | undefined) ?? (extra?.automatic_payment_methods as { enabled: true } | undefined) ??
@@ -125,7 +132,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
handleStripeError(error: any): HandledErrorType { handleStripeError(error: any): HandledErrorType {
switch (error.type) { switch (error.type) {
case 'StripeCardError': case "StripeCardError":
// Stripe has created a payment intent but it failed // Stripe has created a payment intent but it failed
// Extract and return paymentIntent object to be stored in payment_session // Extract and return paymentIntent object to be stored in payment_session
// Allows for reference to the failed intent and potential webhook reconciliation // Allows for reference to the failed intent and potential webhook reconciliation
@@ -133,7 +140,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
if (stripeError.payment_intent) { if (stripeError.payment_intent) {
return { return {
retry: false, retry: false,
data: stripeError.payment_intent data: stripeError.payment_intent,
} }
} else { } else {
throw this.buildError( throw this.buildError(
@@ -142,21 +149,21 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
) )
} }
case 'StripeConnectionError': case "StripeConnectionError":
case 'StripeRateLimitError': case "StripeRateLimitError":
// Connection or rate limit errors indicate an uncertain result // Connection or rate limit errors indicate an uncertain result
// Retry the operation // Retry the operation
return { return {
retry: true, retry: true,
} }
case 'StripeAPIError': { case "StripeAPIError": {
// API errors should be treated as indeterminate per Stripe documentation // API errors should be treated as indeterminate per Stripe documentation
// Rely on webhooks rather than assuming failure // Rely on webhooks rather than assuming failure
return { return {
retry: false, retry: false,
data: { data: {
indeterminate_due_to: "stripe_api_error" indeterminate_due_to: "stripe_api_error",
} },
} }
} }
default: default:
@@ -177,23 +184,28 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
currentAttempt: number = 1 currentAttempt: number = 1
): Promise<T | StripeErrorData> { ): Promise<T | StripeErrorData> {
try { try {
return await apiCall(); return await apiCall()
} catch (error) { } catch (error) {
const handledError = this.handleStripeError(error) const handledError = this.handleStripeError(error)
if (!handledError.retry) { if (!handledError.retry) {
// If retry is false, we know data exists per the type definition // If retry is false, we know data exists per the type definition
return handledError.data; return handledError.data
} }
if ( if (handledError.retry && currentAttempt <= maxRetries) {
handledError.retry &&
currentAttempt <= maxRetries
) {
// Logic for retrying // Logic for retrying
const delay = baseDelay * Math.pow(2, currentAttempt - 1) * (0.5 + Math.random() * 0.5); const delay =
await setTimeout(delay); baseDelay *
return this.executeWithRetry(apiCall, maxRetries, baseDelay, currentAttempt + 1); 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 // Retries are exhausted
throw this.buildError( throw this.buildError(
@@ -203,7 +215,6 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
} }
} }
async getPaymentStatus({ async getPaymentStatus({
data, data,
}: GetPaymentStatusInput): Promise<GetPaymentStatusOutput> { }: GetPaymentStatusInput): Promise<GetPaymentStatusOutput> {
@@ -262,12 +273,12 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
| string | string
| undefined | undefined
const sessionData = await this.executeWithRetry<Stripe.PaymentIntent>( const sessionData = await this.executeWithRetry<Stripe.PaymentIntent>(() =>
() => this.stripe_.paymentIntents.create(intentRequest, { this.stripe_.paymentIntents.create(intentRequest, {
idempotencyKey: context?.idempotency_key, idempotencyKey: context?.idempotency_key,
}) })
) )
const isPaymentIntent = 'id' in sessionData; const isPaymentIntent = "id" in sessionData
return { return {
id: isPaymentIntent ? sessionData.id : (data?.session_id as string), id: isPaymentIntent ? sessionData.id : (data?.session_id as string),
data: sessionData as unknown as Record<string, unknown>, data: sessionData as unknown as Record<string, unknown>,