feat: Improvements to payment module and Stripe provider (#10980)

* fix: Correctly parse Stripe error, remove unused method

* fix: Isolate the payment provider error check function

* fix: Allow passing few extra parameters to Stripe

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Stevche RadevskiandOli Juhl authored and GitHub committed 2025-01-16 08:12:53 +01:00
1 parent 44cd0224af
commit f5235862c0
4 files changed
+51 -85

No files matched your search

@@ -697,16 +697,3 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
data: ProviderWebhookPayload["payload"] data: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> ): Promise<WebhookActionResult>
} }
/**
* @ignore
*/
export function isPaymentProviderError(obj: any): obj is PaymentProviderError {
return (
obj &&
typeof obj === "object" &&
"error" in obj &&
"code" in obj &&
"detail" in obj
)
}
@@ -556,7 +556,6 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
data: {}, data: {},
context: { context: {
extra: {}, extra: {},
resource_id: "test",
email: "test@test.com", email: "test@test.com",
billing_address: {}, billing_address: {},
customer: {}, customer: {},
@@ -13,11 +13,7 @@ import {
UpdatePaymentProviderSession, UpdatePaymentProviderSession,
WebhookActionResult, WebhookActionResult,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import { MedusaError, ModulesSdkUtils } from "@medusajs/framework/utils"
isPaymentProviderError,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/framework/utils"
import { PaymentProvider } from "@models" import { PaymentProvider } from "@models"
import { EOL } from "os" import { EOL } from "os"
@@ -171,3 +167,13 @@ Please make sure that the provider is registered in the container and it is conf
) )
} }
} }
function isPaymentProviderError(obj: any): obj is PaymentProviderError {
return (
obj &&
typeof obj === "object" &&
"error" in obj &&
"code" in obj &&
"detail" in obj
)
}
@@ -1,5 +1,3 @@
import { EOL } from "os"
import Stripe from "stripe" import Stripe from "stripe"
import { import {
@@ -13,9 +11,7 @@ import {
import { import {
AbstractPaymentProvider, AbstractPaymentProvider,
isDefined, isDefined,
isPaymentProviderError,
isPresent, isPresent,
MedusaError,
PaymentActions, PaymentActions,
PaymentSessionStatus, PaymentSessionStatus,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
@@ -60,23 +56,37 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return this.options_ return this.options_
} }
getPaymentIntentOptions(): PaymentIntentOptions { normalizePaymentIntentParameters(
const options: PaymentIntentOptions = {} extra?: Record<string, unknown>
): Partial<Stripe.PaymentIntentCreateParams> {
const res = {} as Partial<Stripe.PaymentIntentCreateParams>
if (this?.paymentIntentOptions?.capture_method) { res.description = (extra?.payment_description ??
options.capture_method = this.paymentIntentOptions.capture_method this.options_?.paymentDescription) as string
}
if (this?.paymentIntentOptions?.setup_future_usage) { res.capture_method =
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage (extra?.capture_method as "automatic" | "manual") ??
} this.paymentIntentOptions.capture_method ??
(this.options_.capture ? "automatic" : "manual")
if (this?.paymentIntentOptions?.payment_method_types) { res.setup_future_usage =
options.payment_method_types = (extra?.setup_future_usage as "off_session" | "on_session" | undefined) ??
this.paymentIntentOptions.payment_method_types this.paymentIntentOptions.setup_future_usage
}
return options res.payment_method_types = this.paymentIntentOptions
.payment_method_types as string[]
res.automatic_payment_methods =
(extra?.automatic_payment_methods as { enabled: true } | undefined) ??
(this.options_?.automaticPaymentMethods ? { enabled: true } : undefined)
res.off_session = extra?.off_session as boolean | undefined
res.confirm = extra?.confirm as boolean | undefined
res.payment_method = extra?.payment_method as string | undefined
return res
} }
async getPaymentStatus( async getPaymentStatus(
@@ -106,24 +116,16 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async initiatePayment( async initiatePayment(
input: CreatePaymentProviderSession input: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> { ): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const intentRequestData = this.getPaymentIntentOptions()
const { email, extra, session_id, customer } = input.context const { email, extra, session_id, customer } = input.context
const { currency_code, amount } = input const { currency_code, amount } = input
const description = (extra?.payment_description ?? const additionalParameters = this.normalizePaymentIntentParameters(extra)
this.options_?.paymentDescription) as string
const intentRequest: Stripe.PaymentIntentCreateParams = { const intentRequest: Stripe.PaymentIntentCreateParams = {
description,
amount: getSmallestUnit(amount, currency_code), amount: getSmallestUnit(amount, currency_code),
currency: currency_code, currency: currency_code,
metadata: { session_id: session_id! }, metadata: { session_id: session_id! },
capture_method: this.options_.capture ? "automatic" : "manual", ...additionalParameters,
...intentRequestData,
}
if (this.options_?.automaticPaymentMethods) {
intentRequest.automatic_payment_methods = { enabled: true }
} }
if (customer?.metadata?.stripe_id) { if (customer?.metadata?.stripe_id) {
@@ -273,15 +275,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
const stripeId = context.customer?.metadata?.stripe_id const stripeId = context.customer?.metadata?.stripe_id
if (stripeId !== data.customer) { if (stripeId !== data.customer) {
const result = await this.initiatePayment(input) return await this.initiatePayment(input)
if (isPaymentProviderError(result)) {
return this.buildError(
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
result
)
}
return result
} else { } else {
if (isPresent(amount) && data.amount === amountNumeric) { if (isPresent(amount) && data.amount === amountNumeric) {
return { data } return { data }
@@ -300,25 +294,6 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
} }
} }
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (isPresent(data.amount)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return (await this.stripe_.paymentIntents.update(sessionId, {
...data,
})) as unknown as PaymentProviderSessionResponse["data"]
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", e)
}
}
async getWebhookActionAndData( async getWebhookActionAndData(
webhookData: ProviderWebhookPayload["payload"] webhookData: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> { ): Promise<WebhookActionResult> {
@@ -374,18 +349,17 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
this.options_.webhookSecret this.options_.webhookSecret
) )
} }
protected buildError( protected buildError(message: string, error: Error): PaymentProviderError {
message: string, const errorDetails =
error: Stripe.StripeRawError | PaymentProviderError | Error "raw" in error ? (error.raw as Stripe.StripeRawError) : error
): PaymentProviderError {
return { return {
error: message, error: `${message}: ${error.message}`,
code: "code" in error ? error.code : "unknown", code: "code" in errorDetails ? errorDetails.code : "unknown",
detail: isPaymentProviderError(error) detail:
? `${error.error}${EOL}${error.detail ?? ""}` "detail" in errorDetails
: "detail" in error ? `${error.message}: ${errorDetails.detail}`
? error.detail : error.message,
: error.message ?? "",
} }
} }
} }