Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improvements to payment module and Stripe provider #10980

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 11 additions & 24 deletions packages/core/utils/src/payment/abstract-payment-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
static validateOptions(options: Record<any, any>): void | never {}

/**
* The constructor allows you to access resources from the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container)
* The constructor allows you to access resources from the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container)
* using the first parameter, and the module's options using the second parameter.
*
*
* :::note
*
*
* A module's options are passed when you register it in the Medusa application.
*
*
* :::
*
* @param {Record<string, unknown>} cradle - The module's container cradle used to resolve resources.
Expand All @@ -55,35 +55,35 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
* @example
* import { AbstractPaymentProvider } from "@medusajs/framework/utils"
* import { Logger } from "@medusajs/framework/types"
*
*
* type Options = {
* apiKey: string
* }
*
*
* type InjectedDependencies = {
* logger: Logger
* }
*
*
* class MyPaymentProviderService extends AbstractPaymentProvider<Options> {
* protected logger_: Logger
* protected options_: Options
* // assuming you're initializing a client
* protected client
*
*
* constructor(
* container: InjectedDependencies,
* options: Options
* ) {
* super(container, options)
*
*
* this.logger_ = container.logger
* this.options_ = options
*
*
* // TODO initialize your client
* }
* // ...
* }
*
*
* export default MyPaymentProviderService
*/
protected constructor(
Expand Down Expand Up @@ -697,16 +697,3 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
data: ProviderWebhookPayload["payload"]
): 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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,6 @@ moduleIntegrationTestRunner<IPaymentModuleService>({
data: {},
context: {
extra: {},
resource_id: "test",
email: "[email protected]",
billing_address: {},
customer: {},
Expand Down
16 changes: 11 additions & 5 deletions packages/modules/payment/src/services/payment-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,7 @@ import {
UpdatePaymentProviderSession,
WebhookActionResult,
} from "@medusajs/framework/types"
import {
isPaymentProviderError,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/framework/utils"
import { MedusaError, ModulesSdkUtils } from "@medusajs/framework/utils"
import { PaymentProvider } from "@models"
import { EOL } from "os"

Expand Down Expand Up @@ -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
)
}
106 changes: 40 additions & 66 deletions packages/modules/providers/payment-stripe/src/core/stripe-base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { EOL } from "os"

import Stripe from "stripe"

import {
Expand All @@ -13,9 +11,7 @@ import {
import {
AbstractPaymentProvider,
isDefined,
isPaymentProviderError,
isPresent,
MedusaError,
PaymentActions,
PaymentSessionStatus,
} from "@medusajs/framework/utils"
Expand Down Expand Up @@ -60,23 +56,37 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
return this.options_
}

getPaymentIntentOptions(): PaymentIntentOptions {
const options: PaymentIntentOptions = {}
normalizePaymentIntentParameters(
extra?: Record<string, unknown>
): Partial<Stripe.PaymentIntentCreateParams> {
const res = {} as Partial<Stripe.PaymentIntentCreateParams>

if (this?.paymentIntentOptions?.capture_method) {
options.capture_method = this.paymentIntentOptions.capture_method
}
res.description = (extra?.payment_description ??
this.options_?.paymentDescription) as string

if (this?.paymentIntentOptions?.setup_future_usage) {
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage
}
res.capture_method =
(extra?.capture_method as "automatic" | "manual") ??
this.paymentIntentOptions.capture_method ??
(this.options_.capture ? "automatic" : "manual")

if (this?.paymentIntentOptions?.payment_method_types) {
options.payment_method_types =
this.paymentIntentOptions.payment_method_types
}
res.setup_future_usage =
(extra?.setup_future_usage as "off_session" | "on_session" | undefined) ??
this.paymentIntentOptions.setup_future_usage

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)

return options
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(
Expand Down Expand Up @@ -106,24 +116,16 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
async initiatePayment(
input: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const intentRequestData = this.getPaymentIntentOptions()
const { email, extra, session_id, customer } = input.context
const { currency_code, amount } = input

const description = (extra?.payment_description ??
this.options_?.paymentDescription) as string
const additionalParameters = this.normalizePaymentIntentParameters(extra)

const intentRequest: Stripe.PaymentIntentCreateParams = {
description,
amount: getSmallestUnit(amount, currency_code),
currency: currency_code,
metadata: { session_id: session_id! },
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}

if (this.options_?.automaticPaymentMethods) {
intentRequest.automatic_payment_methods = { enabled: true }
...additionalParameters,
}

if (customer?.metadata?.stripe_id) {
Expand Down Expand Up @@ -273,15 +275,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
const stripeId = context.customer?.metadata?.stripe_id

if (stripeId !== data.customer) {
const result = 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
return await this.initiatePayment(input)
} else {
if (isPresent(amount) && data.amount === amountNumeric) {
return { data }
Expand All @@ -300,25 +294,6 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
}
}

async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not part of the provider interface

// 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(
webhookData: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {
Expand Down Expand Up @@ -374,18 +349,17 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeOptions> {
this.options_.webhookSecret
)
}
protected buildError(
message: string,
error: Stripe.StripeRawError | PaymentProviderError | Error
): PaymentProviderError {
protected buildError(message: string, error: Error): PaymentProviderError {
const errorDetails =
"raw" in error ? (error.raw as Stripe.StripeRawError) : error

return {
error: message,
code: "code" in error ? error.code : "unknown",
detail: isPaymentProviderError(error)
? `${error.error}${EOL}${error.detail ?? ""}`
: "detail" in error
? error.detail
: error.message ?? "",
error: `${message}: ${error.message}`,
code: "code" in errorDetails ? errorDetails.code : "unknown",
detail:
"detail" in errorDetails
? `${error.message}: ${errorDetails.detail}`
: error.message,
}
}
}
Expand Down
Loading