chore(): Modules providers reorganization (#7234)
Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
co-authored by
Riqwan Thamir
parent
2f7b53488d
commit
93f6e60c17
@@ -0,0 +1,381 @@
|
||||
import { EOL } from "os"
|
||||
|
||||
import Stripe from "stripe"
|
||||
|
||||
import {
|
||||
MedusaContainer,
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
UpdatePaymentProviderSession,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
AbstractPaymentProvider,
|
||||
BigNumber,
|
||||
MedusaError,
|
||||
PaymentActions,
|
||||
isPaymentProviderError,
|
||||
} from "@medusajs/utils"
|
||||
import { isDefined } from "medusa-core-utils"
|
||||
|
||||
import { CreatePaymentProviderSession } from "@medusajs/types"
|
||||
import {
|
||||
ErrorCodes,
|
||||
ErrorIntentStatus,
|
||||
PaymentIntentOptions,
|
||||
StripeCredentials,
|
||||
StripeOptions,
|
||||
} from "../types"
|
||||
|
||||
abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
||||
protected readonly options_: StripeOptions
|
||||
protected stripe_: Stripe
|
||||
protected container_: MedusaContainer
|
||||
|
||||
protected constructor(container: MedusaContainer, options: StripeOptions) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
|
||||
this.container_ = container
|
||||
this.options_ = options
|
||||
|
||||
this.stripe_ = this.init()
|
||||
}
|
||||
|
||||
protected init() {
|
||||
this.validateOptions(this.config)
|
||||
|
||||
return new Stripe(this.config.apiKey)
|
||||
}
|
||||
|
||||
abstract get paymentIntentOptions(): PaymentIntentOptions
|
||||
|
||||
private validateOptions(options: StripeCredentials): void {
|
||||
if (!isDefined(options.apiKey)) {
|
||||
throw new Error("Required option `apiKey` is missing in Stripe plugin")
|
||||
}
|
||||
}
|
||||
|
||||
get options(): StripeOptions {
|
||||
return this.options_
|
||||
}
|
||||
|
||||
getPaymentIntentOptions(): PaymentIntentOptions {
|
||||
const options: PaymentIntentOptions = {}
|
||||
|
||||
if (this?.paymentIntentOptions?.capture_method) {
|
||||
options.capture_method = this.paymentIntentOptions.capture_method
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.setup_future_usage) {
|
||||
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.payment_method_types) {
|
||||
options.payment_method_types =
|
||||
this.paymentIntentOptions.payment_method_types
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
async getPaymentStatus(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentSessionStatus> {
|
||||
const id = paymentSessionData.id as string
|
||||
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
|
||||
|
||||
switch (paymentIntent.status) {
|
||||
case "requires_payment_method":
|
||||
case "requires_confirmation":
|
||||
case "processing":
|
||||
return PaymentSessionStatus.PENDING
|
||||
case "requires_action":
|
||||
return PaymentSessionStatus.REQUIRES_MORE
|
||||
case "canceled":
|
||||
return PaymentSessionStatus.CANCELED
|
||||
case "requires_capture":
|
||||
case "succeeded":
|
||||
return PaymentSessionStatus.AUTHORIZED
|
||||
default:
|
||||
return PaymentSessionStatus.PENDING
|
||||
}
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
input: CreatePaymentProviderSession
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||
const intentRequestData = this.getPaymentIntentOptions()
|
||||
const { email, extra, resource_id, customer } = input.context
|
||||
const { currency_code, amount } = input
|
||||
|
||||
const description = (extra?.payment_description ??
|
||||
this.options_?.payment_description) as string
|
||||
|
||||
const intentRequest: Stripe.PaymentIntentCreateParams = {
|
||||
description,
|
||||
amount: Math.round(new BigNumber(amount).numeric),
|
||||
currency: currency_code,
|
||||
metadata: { resource_id: resource_id ?? "Medusa Payment" },
|
||||
capture_method: this.options_.capture ? "automatic" : "manual",
|
||||
...intentRequestData,
|
||||
}
|
||||
|
||||
if (this.options_?.automatic_payment_methods) {
|
||||
intentRequest.automatic_payment_methods = { enabled: true }
|
||||
}
|
||||
|
||||
if (customer?.metadata?.stripe_id) {
|
||||
intentRequest.customer = customer.metadata.stripe_id as string
|
||||
} else {
|
||||
let stripeCustomer
|
||||
try {
|
||||
stripeCustomer = await this.stripe_.customers.create({
|
||||
email,
|
||||
})
|
||||
} catch (e) {
|
||||
return this.buildError(
|
||||
"An error occurred in initiatePayment when creating a Stripe customer",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
intentRequest.customer = stripeCustomer.id
|
||||
}
|
||||
|
||||
let sessionData
|
||||
try {
|
||||
sessionData = (await this.stripe_.paymentIntents.create(
|
||||
intentRequest
|
||||
)) as unknown as Record<string, unknown>
|
||||
} catch (e) {
|
||||
return this.buildError(
|
||||
"An error occurred in InitiatePayment during the creation of the stripe payment intent",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
data: sessionData,
|
||||
// TODO: REVISIT
|
||||
// update_requests: customer?.metadata?.stripe_id
|
||||
// ? undefined
|
||||
// : {
|
||||
// customer_metadata: {
|
||||
// stripe_id: intentRequest.customer,
|
||||
// },
|
||||
// },
|
||||
}
|
||||
}
|
||||
|
||||
async authorizePayment(
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
context: Record<string, unknown>
|
||||
): Promise<
|
||||
| PaymentProviderError
|
||||
| {
|
||||
status: PaymentSessionStatus
|
||||
data: PaymentProviderSessionResponse["data"]
|
||||
}
|
||||
> {
|
||||
const status = await this.getPaymentStatus(paymentSessionData)
|
||||
return { data: paymentSessionData, status }
|
||||
}
|
||||
|
||||
async cancelPayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
|
||||
try {
|
||||
const id = paymentSessionData.id as string
|
||||
return (await this.stripe_.paymentIntents.cancel(
|
||||
id
|
||||
)) as unknown as PaymentProviderSessionResponse["data"]
|
||||
} catch (error) {
|
||||
if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
|
||||
return error.payment_intent
|
||||
}
|
||||
|
||||
return this.buildError("An error occurred in cancelPayment", error)
|
||||
}
|
||||
}
|
||||
|
||||
async capturePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
|
||||
const id = paymentSessionData.id as string
|
||||
try {
|
||||
const intent = await this.stripe_.paymentIntents.capture(id)
|
||||
return intent as unknown as PaymentProviderSessionResponse["data"]
|
||||
} catch (error) {
|
||||
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
|
||||
if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) {
|
||||
return error.payment_intent
|
||||
}
|
||||
}
|
||||
|
||||
return this.buildError("An error occurred in capturePayment", error)
|
||||
}
|
||||
}
|
||||
|
||||
async deletePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
|
||||
return await this.cancelPayment(paymentSessionData)
|
||||
}
|
||||
|
||||
async refundPayment(
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
refundAmount: number
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
|
||||
const id = paymentSessionData.id as string
|
||||
|
||||
try {
|
||||
await this.stripe_.refunds.create({
|
||||
amount: Math.round(refundAmount),
|
||||
payment_intent: id as string,
|
||||
})
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in refundPayment", e)
|
||||
}
|
||||
|
||||
return paymentSessionData
|
||||
}
|
||||
|
||||
async retrievePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
|
||||
try {
|
||||
const id = paymentSessionData.id as string
|
||||
const intent = await this.stripe_.paymentIntents.retrieve(id)
|
||||
return intent as unknown as PaymentProviderSessionResponse["data"]
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in retrievePayment", e)
|
||||
}
|
||||
}
|
||||
|
||||
async updatePayment(
|
||||
input: UpdatePaymentProviderSession
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||
const { context, data, amount } = input
|
||||
|
||||
const amountNumeric = Math.round(new BigNumber(amount).numeric)
|
||||
|
||||
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
|
||||
} else {
|
||||
if (amount && data.amount === amountNumeric) {
|
||||
return { data }
|
||||
}
|
||||
|
||||
try {
|
||||
const id = data.id as string
|
||||
const sessionData = (await this.stripe_.paymentIntents.update(id, {
|
||||
amount: amountNumeric,
|
||||
})) as unknown as PaymentProviderSessionResponse["data"]
|
||||
|
||||
return { data: sessionData }
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in updatePayment", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (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> {
|
||||
const event = this.constructWebhookEvent(webhookData)
|
||||
const intent = event.data.object as Stripe.PaymentIntent
|
||||
|
||||
switch (event.type) {
|
||||
case "payment_intent.amount_capturable_updated":
|
||||
return {
|
||||
action: PaymentActions.AUTHORIZED,
|
||||
data: {
|
||||
resource_id: intent.metadata.resource_id,
|
||||
amount: intent.amount_capturable, // NOTE: revisit when implementing multicapture
|
||||
},
|
||||
}
|
||||
case "payment_intent.succeeded":
|
||||
return {
|
||||
action: PaymentActions.SUCCESSFUL,
|
||||
data: {
|
||||
resource_id: intent.metadata.resource_id,
|
||||
amount: intent.amount_received,
|
||||
},
|
||||
}
|
||||
case "payment_intent.payment_failed":
|
||||
return {
|
||||
action: PaymentActions.FAILED,
|
||||
data: {
|
||||
resource_id: intent.metadata.resource_id,
|
||||
amount: intent.amount,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return { action: PaymentActions.NOT_SUPPORTED }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs Stripe Webhook event
|
||||
* @param {object} data - the data of the webhook request: req.body
|
||||
* ensures integrity of the webhook event
|
||||
* @return {object} Stripe Webhook event
|
||||
*/
|
||||
constructWebhookEvent(data: ProviderWebhookPayload["payload"]): Stripe.Event {
|
||||
const signature = data.headers["stripe-signature"] as string
|
||||
|
||||
return this.stripe_.webhooks.constructEvent(
|
||||
data.rawData as string | Buffer,
|
||||
signature,
|
||||
this.config.webhookSecret
|
||||
)
|
||||
}
|
||||
protected buildError(
|
||||
message: string,
|
||||
error: Stripe.StripeRawError | PaymentProviderError | Error
|
||||
): PaymentProviderError {
|
||||
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 ?? "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeBase
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ModuleProviderExports } from "@medusajs/types"
|
||||
import {
|
||||
StripeBancontactService,
|
||||
StripeBlikService,
|
||||
StripeGiropayService,
|
||||
StripeIdealService,
|
||||
StripeProviderService,
|
||||
StripePrzelewy24Service,
|
||||
} from "./services"
|
||||
|
||||
const services = [
|
||||
StripeBancontactService,
|
||||
StripeBlikService,
|
||||
StripeGiropayService,
|
||||
StripeIdealService,
|
||||
StripeProviderService,
|
||||
StripePrzelewy24Service,
|
||||
]
|
||||
|
||||
const providerExport: ModuleProviderExports = {
|
||||
services,
|
||||
}
|
||||
|
||||
export default providerExport
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as StripeBancontactService } from "./stripe-bancontact"
|
||||
export { default as StripeBlikService } from "./stripe-blik"
|
||||
export { default as StripeGiropayService } from "./stripe-giropay"
|
||||
export { default as StripeIdealService } from "./stripe-ideal"
|
||||
export { default as StripeProviderService } from "./stripe-provider"
|
||||
export { default as StripePrzelewy24Service } from "./stripe-przelewy24"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class BancontactProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.BAN_CONTACT
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["bancontact"],
|
||||
capture_method: "automatic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BancontactProviderService
|
||||
@@ -0,0 +1,19 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class BlikProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.BLIK
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["blik"],
|
||||
capture_method: "automatic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BlikProviderService
|
||||
@@ -0,0 +1,19 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class GiropayProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.GIROPAY
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["giropay"],
|
||||
capture_method: "automatic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default GiropayProviderService
|
||||
@@ -0,0 +1,19 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class IdealProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.IDEAL
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["ideal"],
|
||||
capture_method: "automatic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default IdealProviderService
|
||||
@@ -0,0 +1,16 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class StripeProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.STRIPE
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeProviderService
|
||||
@@ -0,0 +1,19 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class Przelewy24ProviderService extends StripeBase {
|
||||
static PROVIDER = PaymentProviderKeys.PRZELEWY_24
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["p24"],
|
||||
capture_method: "automatic",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Przelewy24ProviderService
|
||||
@@ -0,0 +1,44 @@
|
||||
export interface StripeCredentials {
|
||||
apiKey: string
|
||||
webhookSecret: string
|
||||
}
|
||||
|
||||
export interface StripeOptions {
|
||||
credentials: Record<string, StripeCredentials>
|
||||
/**
|
||||
* Use this flag to capture payment immediately (default is false)
|
||||
*/
|
||||
capture?: boolean
|
||||
/**
|
||||
* set `automatic_payment_methods` to `{ enabled: true }`
|
||||
*/
|
||||
automatic_payment_methods?: boolean
|
||||
/**
|
||||
* Set a default description on the intent if the context does not provide one
|
||||
*/
|
||||
payment_description?: string
|
||||
}
|
||||
|
||||
export interface PaymentIntentOptions {
|
||||
capture_method?: "automatic" | "manual"
|
||||
setup_future_usage?: "on_session" | "off_session"
|
||||
payment_method_types?: string[]
|
||||
}
|
||||
|
||||
export const ErrorCodes = {
|
||||
PAYMENT_INTENT_UNEXPECTED_STATE: "payment_intent_unexpected_state",
|
||||
}
|
||||
|
||||
export const ErrorIntentStatus = {
|
||||
SUCCEEDED: "succeeded",
|
||||
CANCELED: "canceled",
|
||||
}
|
||||
|
||||
export const PaymentProviderKeys = {
|
||||
STRIPE: "stripe",
|
||||
BAN_CONTACT: "stripe-bancontact",
|
||||
BLIK: "stripe-blik",
|
||||
GIROPAY: "stripe-giropay",
|
||||
IDEAL: "stripe-ideal",
|
||||
PRZELEWY_24: "stripe-przelewy24",
|
||||
}
|
||||
Reference in New Issue
Block a user