feat(payment, payment-stripe): Add Stripe module provider (#6311)
This commit is contained in:
@@ -5,17 +5,23 @@ import {
|
||||
MODULE_PACKAGE_NAMES,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IPaymentModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
import {
|
||||
IPaymentModuleService,
|
||||
ModuleProvider,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
| (
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration
|
||||
) & { providers: ModuleProvider[] },
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<IPaymentModuleService> => {
|
||||
const loaded = await MedusaModule.bootstrap<IPaymentModuleService>({
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import { moduleProviderLoader } from "@medusajs/modules-sdk"
|
||||
|
||||
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { Lifetime, asFunction } from "awilix"
|
||||
import { Lifetime, asFunction, asValue } from "awilix"
|
||||
|
||||
import * as providers from "../providers"
|
||||
|
||||
const registrationFn = async (klass, container, pluginOptions) => {
|
||||
container.register({
|
||||
[`pp_${klass.identifier}`]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginOptions),
|
||||
{
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
})
|
||||
Object.entries(pluginOptions.config || []).map(([name, config]) => {
|
||||
const key = `pp_${klass.PROVIDER}_${name}`
|
||||
|
||||
container.registerAdd(
|
||||
"payment_providers",
|
||||
asFunction((cradle) => new klass(cradle, pluginOptions), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
container.register({
|
||||
[key]: asFunction((cradle) => new klass(cradle, config), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
container.registerAdd("payment_providers", asValue(key))
|
||||
})
|
||||
}
|
||||
|
||||
export default async ({
|
||||
@@ -34,7 +29,7 @@ export default async ({
|
||||
>): Promise<void> => {
|
||||
// Local providers
|
||||
for (const provider of Object.values(providers)) {
|
||||
await registrationFn(provider, container, {})
|
||||
await registrationFn(provider, container, { config: { default: {} } })
|
||||
}
|
||||
|
||||
await moduleProviderLoader({
|
||||
|
||||
@@ -14,11 +14,7 @@ import {
|
||||
} from "@mikro-orm/core"
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
import {
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
optionalNumericSerializer,
|
||||
} from "@medusajs/utils"
|
||||
import { DALUtils, generateEntityId } from "@medusajs/utils"
|
||||
import Refund from "./refund"
|
||||
import Capture from "./capture"
|
||||
import PaymentSession from "./payment-session"
|
||||
@@ -40,13 +36,6 @@ export default class Payment {
|
||||
})
|
||||
amount: number
|
||||
|
||||
@Property({
|
||||
columnType: "numeric",
|
||||
nullable: true,
|
||||
serializer: optionalNumericSerializer,
|
||||
})
|
||||
authorized_amount: number | null = null
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
currency_code: string
|
||||
|
||||
@@ -119,7 +108,11 @@ export default class Payment {
|
||||
})
|
||||
payment_collection!: PaymentCollection
|
||||
|
||||
@OneToOne({ owner: true, fieldName: "session_id" })
|
||||
@OneToOne({
|
||||
owner: true,
|
||||
fieldName: "session_id",
|
||||
index: "IDX_payment_payment_session_id",
|
||||
})
|
||||
payment_session!: PaymentSession
|
||||
|
||||
/** COMPUTED PROPERTIES START **/
|
||||
|
||||
@@ -3,11 +3,14 @@ import {
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractPaymentProvider } from "@medusajs/utils"
|
||||
import { AbstractPaymentProvider, PaymentActions } from "@medusajs/utils"
|
||||
|
||||
export class SystemProviderService extends AbstractPaymentProvider {
|
||||
static identifier = "system"
|
||||
static PROVIDER = "system"
|
||||
|
||||
async getStatus(_): Promise<string> {
|
||||
return "authorized"
|
||||
@@ -66,6 +69,12 @@ export class SystemProviderService extends AbstractPaymentProvider {
|
||||
async cancelPayment(_): Promise<Record<string, unknown>> {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(
|
||||
data: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult> {
|
||||
return { action: PaymentActions.NOT_SUPPORTED }
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemProviderService
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as PaymentModuleService } from "./payment-module"
|
||||
export { default as PaymentProviderService } from "./payment-provider"
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Context,
|
||||
CreateCaptureDTO,
|
||||
CreatePaymentCollectionDTO,
|
||||
CreatePaymentDTO,
|
||||
CreatePaymentProviderDTO,
|
||||
CreatePaymentSessionDTO,
|
||||
CreateRefundDTO,
|
||||
@@ -16,12 +15,14 @@ import {
|
||||
PaymentDTO,
|
||||
PaymentSessionDTO,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
RefundDTO,
|
||||
UpdatePaymentCollectionDTO,
|
||||
UpdatePaymentDTO,
|
||||
UpdatePaymentSessionDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
PaymentActions,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
@@ -206,23 +207,38 @@ export default class PaymentModuleService<
|
||||
data: CreatePaymentSessionDTO,
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentSessionDTO> {
|
||||
const sessionData = await this.paymentProviderService_.createSession(
|
||||
data.provider_id,
|
||||
data.providerContext
|
||||
)
|
||||
|
||||
const created = await this.paymentSessionService_.create(
|
||||
{
|
||||
provider_id: data.provider_id,
|
||||
amount: data.providerContext.amount,
|
||||
currency_code: data.providerContext.currency_code,
|
||||
payment_collection: paymentCollectionId,
|
||||
data: sessionData,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize(created, { populate: true })
|
||||
try {
|
||||
const sessionData = await this.paymentProviderService_.createSession(
|
||||
data.provider_id,
|
||||
{
|
||||
...data.providerContext,
|
||||
resource_id: created.id,
|
||||
}
|
||||
)
|
||||
|
||||
await this.paymentSessionService_.update(
|
||||
{
|
||||
id: created.id,
|
||||
data: sessionData,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize(created, { populate: true })
|
||||
} catch (e) {
|
||||
await this.paymentSessionService_.delete([created.id], sharedContext)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
@@ -288,6 +304,7 @@ export default class PaymentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// this method needs to be idempotent
|
||||
if (session.authorized_at) {
|
||||
const payment = await this.paymentService_.retrieve(
|
||||
{ session_id: session.id },
|
||||
@@ -330,7 +347,6 @@ export default class PaymentModuleService<
|
||||
{
|
||||
amount: session.amount,
|
||||
currency_code: session.currency_code,
|
||||
authorized_amount: session.amount,
|
||||
payment_session: session.id,
|
||||
payment_collection: session.payment_collection!.id,
|
||||
provider_id: session.provider_id,
|
||||
@@ -374,15 +390,17 @@ export default class PaymentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
// this method needs to be idempotent
|
||||
if (payment.captured_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`The payment: ${payment.id} is already fully captured.`
|
||||
return this.retrievePayment(
|
||||
data.payment_id,
|
||||
{ relations: ["captures"] },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
|
||||
// if (payment.captured_amount + input.amount > payment.authorized_amount) {
|
||||
// if (payment.captured_amount + input.amount > payment.amount) {
|
||||
// throw new MedusaError(
|
||||
// MedusaError.Types.INVALID_DATA,
|
||||
// `Total captured amount for payment: ${payment.id} exceeds authorized amount.`
|
||||
@@ -500,25 +518,60 @@ export default class PaymentModuleService<
|
||||
return await this.retrievePayment(payment.id, {}, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async processEvent(
|
||||
eventData: ProviderWebhookPayload,
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<void> {
|
||||
const providerId = `pp_${eventData.provider}`
|
||||
|
||||
const event = await this.paymentProviderService_.getWebhookActionAndData(
|
||||
providerId,
|
||||
eventData.payload
|
||||
)
|
||||
|
||||
if (event.action === PaymentActions.NOT_SUPPORTED) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (event.action) {
|
||||
case PaymentActions.SUCCESSFUL: {
|
||||
const [payment] = await this.listPayments({
|
||||
session_id: event.data.resource_id,
|
||||
})
|
||||
|
||||
await this.capturePayment(
|
||||
{ payment_id: payment.id, amount: event.data.amount },
|
||||
sharedContext
|
||||
)
|
||||
break
|
||||
}
|
||||
case PaymentActions.AUTHORIZED:
|
||||
await this.authorizePaymentSession(
|
||||
event.data.resource_id as string,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async createProvidersOnLoad() {
|
||||
const providersToLoad = this.__container__["payment_providers"]
|
||||
|
||||
const providers = await this.paymentProviderService_.list({
|
||||
// @ts-ignore TODO
|
||||
id: providersToLoad.map((p) => p.getIdentifier()),
|
||||
id: providersToLoad,
|
||||
})
|
||||
|
||||
const loadedProvidersMap = new Map(providers.map((p) => [p.id, p]))
|
||||
|
||||
const providersToCreate: CreatePaymentProviderDTO[] = []
|
||||
for (const provider of providersToLoad) {
|
||||
if (loadedProvidersMap.has(provider.getIdentifier())) {
|
||||
for (const id of providersToLoad) {
|
||||
if (loadedProvidersMap.has(id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
providersToCreate.push({
|
||||
id: provider.getIdentifier(),
|
||||
})
|
||||
providersToCreate.push({ id })
|
||||
}
|
||||
|
||||
await this.paymentProviderService_.create(providersToCreate)
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
@@ -57,7 +59,7 @@ export default class PaymentProviderService {
|
||||
|
||||
retrieveProvider(providerId: string): IPaymentProvider {
|
||||
try {
|
||||
return this.container_[`pp_${providerId}`] as IPaymentProvider
|
||||
return this.container_[providerId] as IPaymentProvider
|
||||
} catch (e) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
@@ -173,6 +175,15 @@ export default class PaymentProviderService {
|
||||
return res as Record<string, unknown>
|
||||
}
|
||||
|
||||
async getWebhookActionAndData(
|
||||
providerId: string,
|
||||
data: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult> {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
|
||||
return await provider.getWebhookActionAndData(data)
|
||||
}
|
||||
|
||||
private throwPaymentProviderError(errObj: PaymentProviderError) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
|
||||
Reference in New Issue
Block a user