feat(payment, payment-stripe): Add Stripe module provider (#6311)
This commit is contained in:
7
.changeset/swift-frogs-collect.md
Normal file
7
.changeset/swift-frogs-collect.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
feat(payment-stripe): new Stripe payment provider
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticationInput,
|
||||
AuthenticationResponse,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils"
|
||||
import { AuthUserService } from "@services"
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
|
||||
import { AuthUserService } from "@services"
|
||||
import { AuthorizationCode } from "simple-oauth2"
|
||||
import url from "url"
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
IdempotencyKeyService,
|
||||
PostgresError,
|
||||
} from "@medusajs/medusa"
|
||||
import { ConfigModule, MedusaContainer } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { EOL } from "os"
|
||||
import Stripe from "stripe"
|
||||
import { StripeOptions } from "../../types"
|
||||
|
||||
const PAYMENT_PROVIDER_KEY = "pp_stripe"
|
||||
|
||||
@@ -179,7 +177,7 @@ async function capturePaymenCollectiontIfNecessary({
|
||||
await manager.transaction(async (manager) => {
|
||||
await paymentCollectionService
|
||||
.withTransaction(manager)
|
||||
.capture(payment.id)
|
||||
.capture(payment.id) // TODO: revisit - this method doesn't exists ATM
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
9
packages/medusa/src/api-v2/hooks/middlewares.ts
Normal file
9
packages/medusa/src/api-v2/hooks/middlewares.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MiddlewareRoute } from "../../types/middlewares"
|
||||
|
||||
export const hooksRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: ["POST"],
|
||||
bodyParser: { preserveRawBody: true },
|
||||
matcher: "/hooks/payment/:provider",
|
||||
},
|
||||
]
|
||||
32
packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts
Normal file
32
packages/medusa/src/api-v2/hooks/payment/[provider]/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { PaymentWebhookEvents } from "@medusajs/utils"
|
||||
import { PaymentModuleOptions } from "@medusajs/types"
|
||||
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../types/routing"
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
try {
|
||||
const { provider } = req.params
|
||||
|
||||
const options: PaymentModuleOptions =
|
||||
req.scope.resolve(ModuleRegistrationName.PAYMENT).options || {}
|
||||
|
||||
const event = {
|
||||
provider,
|
||||
payload: { data: req.body, rawData: req.rawBody, headers: req.headers },
|
||||
}
|
||||
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
|
||||
// we delay the processing of the event to avoid a conflict caused by a race condition
|
||||
await eventBus.emit(PaymentWebhookEvents.WebhookReceived, event, {
|
||||
delay: options.webhook_delay || 5000,
|
||||
attempts: options.webhook_retries || 3,
|
||||
})
|
||||
} catch (err) {
|
||||
res.status(400).send(`Webhook Error: ${err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { authRoutesMiddlewares } from "./auth/middlewares"
|
||||
import { storeCartRoutesMiddlewares } from "./store/carts/middlewares"
|
||||
import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares"
|
||||
import { storeRegionRoutesMiddlewares } from "./store/regions/middlewares"
|
||||
import { hooksRoutesMiddlewares } from "./hooks/middlewares"
|
||||
|
||||
export const config: MiddlewaresConfig = {
|
||||
routes: [
|
||||
@@ -29,5 +30,6 @@ export const config: MiddlewaresConfig = {
|
||||
...adminUserRoutesMiddlewares,
|
||||
...adminInviteRoutesMiddlewares,
|
||||
...adminApiKeyRoutesMiddlewares,
|
||||
...hooksRoutesMiddlewares,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -33,8 +33,6 @@ export default async ({
|
||||
next()
|
||||
})
|
||||
|
||||
app.use(bodyParser.json())
|
||||
|
||||
if (featureFlagRouter?.isFeatureEnabled(FeatureFlagUtils.MedusaV2Flag.key)) {
|
||||
// TODO: Figure out why this is causing issues with test when placed inside ./api.ts
|
||||
// Adding this here temporarily
|
||||
@@ -52,6 +50,7 @@ export default async ({
|
||||
throw Error("An error occurred while registering Medusa Core API Routes")
|
||||
}
|
||||
} else {
|
||||
app.use(bodyParser.json())
|
||||
app.use("/", routes(container, configModule.projectConfig))
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
RouteConfig,
|
||||
RouteDescriptor,
|
||||
RouteVerb,
|
||||
ParserConfigArgs,
|
||||
} from "./types"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||
|
||||
const log = ({
|
||||
activityId,
|
||||
@@ -150,9 +152,18 @@ function findMatch(
|
||||
* Returns an array of body parser middlewares that are applied on routes
|
||||
* out-of-the-box.
|
||||
*/
|
||||
function getBodyParserMiddleware(sizeLimit?: string | number | undefined) {
|
||||
function getBodyParserMiddleware(args?: ParserConfigArgs) {
|
||||
const sizeLimit = args?.sizeLimit
|
||||
const preserveRawBody = args?.preserveRawBody
|
||||
return [
|
||||
json({ limit: sizeLimit }),
|
||||
json({
|
||||
limit: sizeLimit,
|
||||
verify: preserveRawBody
|
||||
? (req: MedusaRequest, res: MedusaResponse, buf: Buffer) => {
|
||||
req.rawBody = buf
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
text({ limit: sizeLimit }),
|
||||
urlencoded({ limit: sizeLimit, extended: true }),
|
||||
]
|
||||
@@ -556,7 +567,7 @@ export class RoutesLoader {
|
||||
|
||||
this.router[method.toLowerCase()](
|
||||
path,
|
||||
...getBodyParserMiddleware(sizeLimit)
|
||||
...getBodyParserMiddleware(mostSpecificConfig?.bodyParser)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -55,12 +55,13 @@ export type MedusaErrorHandlerFunction = (
|
||||
next: MedusaNextFunction
|
||||
) => Promise<void> | void
|
||||
|
||||
type ParserConfig =
|
||||
| false
|
||||
| {
|
||||
export type ParserConfigArgs = {
|
||||
sizeLimit?: string | number | undefined
|
||||
preserveRawBody?: boolean
|
||||
}
|
||||
|
||||
type ParserConfig = false | ParserConfigArgs
|
||||
|
||||
export type MiddlewareRoute = {
|
||||
method?: MiddlewareVerb | MiddlewareVerb[]
|
||||
matcher: string | RegExp
|
||||
|
||||
50
packages/medusa/src/subscribers/payment-webhook.ts
Normal file
50
packages/medusa/src/subscribers/payment-webhook.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { PaymentWebhookEvents } from "@medusajs/utils"
|
||||
|
||||
import {
|
||||
IEventBusService,
|
||||
IPaymentModuleService,
|
||||
ProviderWebhookPayload,
|
||||
Subscriber,
|
||||
} from "@medusajs/types"
|
||||
import { EventBusService } from "../services"
|
||||
|
||||
type SerializedBuffer = {
|
||||
data: ArrayBuffer
|
||||
type: "Buffer"
|
||||
}
|
||||
|
||||
type InjectedDependencies = {
|
||||
paymentModuleService: IPaymentModuleService
|
||||
eventBusService: EventBusService
|
||||
}
|
||||
|
||||
class PaymentWebhookSubscriber {
|
||||
private readonly eventBusService_: IEventBusService
|
||||
private readonly paymentModuleService_: IPaymentModuleService
|
||||
|
||||
constructor({ eventBusService, paymentModuleService }: InjectedDependencies) {
|
||||
this.eventBusService_ = eventBusService
|
||||
this.paymentModuleService_ = paymentModuleService
|
||||
|
||||
this.eventBusService_.subscribe(
|
||||
PaymentWebhookEvents.WebhookReceived,
|
||||
this.processEvent as Subscriber
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: consider moving this to a workflow
|
||||
*/
|
||||
processEvent = async (data: ProviderWebhookPayload): Promise<void> => {
|
||||
if (
|
||||
(data.payload.rawData as unknown as SerializedBuffer).type === "Buffer"
|
||||
) {
|
||||
data.payload.rawData = Buffer.from(
|
||||
(data.payload.rawData as unknown as SerializedBuffer).data
|
||||
)
|
||||
}
|
||||
await this.paymentModuleService_.processEvent(data)
|
||||
}
|
||||
}
|
||||
|
||||
export default PaymentWebhookSubscriber
|
||||
@@ -7,6 +7,7 @@ export interface MedusaRequest extends Request {
|
||||
user?: (User | Customer) & { customer_id?: string; userId?: string }
|
||||
scope: MedusaContainer
|
||||
session?: any
|
||||
rawBody?: any
|
||||
requestId?: string
|
||||
auth_user?: { id: string; app_metadata: Record<string, any>; scope: string }
|
||||
}
|
||||
|
||||
4
packages/payment-stripe/.gitignore
vendored
Normal file
4
packages/payment-stripe/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
node_modules
|
||||
.DS_store
|
||||
yarn.lock
|
||||
0
packages/payment-stripe/CHANGELOG.md
Normal file
0
packages/payment-stripe/CHANGELOG.md
Normal file
0
packages/payment-stripe/README.md
Normal file
0
packages/payment-stripe/README.md
Normal file
13
packages/payment-stripe/jest.config.js
Normal file
13
packages/payment-stripe/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
}
|
||||
48
packages/payment-stripe/package.json
Normal file
48
packages/payment-stripe/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@medusajs/payment-stripe",
|
||||
"version": "0.0.1",
|
||||
"description": "Stripe payment provider for Medusa",
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/payment-stripe"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
|
||||
"test": "jest --passWithNoTests src",
|
||||
"build": "rimraf dist && tsc -p ./tsconfig.json",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/medusa": "^1.19.1",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"awilix": "^8.0.1",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"rimraf": "^5.0.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@medusajs/medusa": "^1.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/utils": "^1.11.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"stripe": "latest"
|
||||
},
|
||||
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
|
||||
"keywords": [
|
||||
"medusa-plugin",
|
||||
"medusa-plugin-payment"
|
||||
]
|
||||
}
|
||||
382
packages/payment-stripe/src/core/stripe-base.ts
Normal file
382
packages/payment-stripe/src/core/stripe-base.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
import { EOL } from "os"
|
||||
|
||||
import Stripe from "stripe"
|
||||
|
||||
import {
|
||||
MedusaContainer,
|
||||
PaymentSessionStatus,
|
||||
PaymentProviderContext,
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
ProviderWebhookPayload,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
PaymentActions,
|
||||
AbstractPaymentProvider,
|
||||
isPaymentProviderError,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import { isDefined } from "medusa-core-utils"
|
||||
|
||||
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(
|
||||
context: PaymentProviderContext
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||
const intentRequestData = this.getPaymentIntentOptions()
|
||||
const {
|
||||
email,
|
||||
context: cart_context,
|
||||
currency_code,
|
||||
amount,
|
||||
resource_id,
|
||||
customer,
|
||||
} = context
|
||||
|
||||
const description = (cart_context.payment_description ??
|
||||
this.options_?.payment_description) as string
|
||||
|
||||
const intentRequest: Stripe.PaymentIntentCreateParams = {
|
||||
description,
|
||||
amount: Math.round(amount),
|
||||
currency: currency_code,
|
||||
metadata: { resource_id },
|
||||
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 session_data
|
||||
try {
|
||||
session_data = (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: session_data,
|
||||
// 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(
|
||||
context: PaymentProviderContext
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||
const { amount, customer, payment_session_data } = context
|
||||
const stripeId = customer?.metadata?.stripe_id
|
||||
|
||||
if (stripeId !== payment_session_data.customer) {
|
||||
const result = await this.initiatePayment(context)
|
||||
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 && payment_session_data.amount === Math.round(amount)) {
|
||||
return { data: payment_session_data }
|
||||
}
|
||||
|
||||
try {
|
||||
const id = payment_session_data.id as string
|
||||
const sessionData = (await this.stripe_.paymentIntents.update(id, {
|
||||
amount: Math.round(amount),
|
||||
})) 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
|
||||
24
packages/payment-stripe/src/index.ts
Normal file
24
packages/payment-stripe/src/index.ts
Normal file
@@ -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
|
||||
7
packages/payment-stripe/src/services/index.ts
Normal file
7
packages/payment-stripe/src/services/index.ts
Normal file
@@ -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"
|
||||
|
||||
19
packages/payment-stripe/src/services/stripe-bancontact.ts
Normal file
19
packages/payment-stripe/src/services/stripe-bancontact.ts
Normal file
@@ -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
|
||||
19
packages/payment-stripe/src/services/stripe-blik.ts
Normal file
19
packages/payment-stripe/src/services/stripe-blik.ts
Normal file
@@ -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
|
||||
19
packages/payment-stripe/src/services/stripe-giropay.ts
Normal file
19
packages/payment-stripe/src/services/stripe-giropay.ts
Normal file
@@ -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
|
||||
19
packages/payment-stripe/src/services/stripe-ideal.ts
Normal file
19
packages/payment-stripe/src/services/stripe-ideal.ts
Normal file
@@ -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
|
||||
16
packages/payment-stripe/src/services/stripe-provider.ts
Normal file
16
packages/payment-stripe/src/services/stripe-provider.ts
Normal file
@@ -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
|
||||
19
packages/payment-stripe/src/services/stripe-przelewy24.ts
Normal file
19
packages/payment-stripe/src/services/stripe-przelewy24.ts
Normal file
@@ -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
|
||||
44
packages/payment-stripe/src/types/index.ts
Normal file
44
packages/payment-stripe/src/types/index.ts
Normal file
@@ -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",
|
||||
}
|
||||
36
packages/payment-stripe/tsconfig.json
Normal file
36
packages/payment-stripe/tsconfig.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"es2019"
|
||||
],
|
||||
"target": "es5",
|
||||
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true, // to use ES5 specific tooling
|
||||
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules",
|
||||
".eslintrc.js"
|
||||
]
|
||||
}
|
||||
5
packages/payment-stripe/tsconfig.spec.json
Normal file
5
packages/payment-stripe/tsconfig.spec.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -24,21 +24,21 @@ export const defaultPaymentSessionData = [
|
||||
id: "pay-sess-id-1",
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
payment_collection: "pay-col-id-1",
|
||||
},
|
||||
{
|
||||
id: "pay-sess-id-2",
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
payment_collection: "pay-col-id-2",
|
||||
},
|
||||
{
|
||||
id: "pay-sess-id-3",
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
payment_collection: "pay-col-id-2",
|
||||
},
|
||||
]
|
||||
@@ -50,7 +50,7 @@ export const defaultPaymentData = [
|
||||
currency_code: "usd",
|
||||
payment_collection: "pay-col-id-1",
|
||||
payment_session: "pay-sess-id-1",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
authorized_amount: 100,
|
||||
data: {},
|
||||
},
|
||||
@@ -61,7 +61,7 @@ export const defaultPaymentData = [
|
||||
currency_code: "usd",
|
||||
payment_collection: "pay-col-id-2",
|
||||
payment_session: "pay-sess-id-2",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
data: {},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { IPaymentModuleService } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { initModules } from "medusa-test-utils"
|
||||
import { MikroOrmWrapper } from "../../utils"
|
||||
import { getInitModuleConfig } from "../../utils/get-init-module-config"
|
||||
import { createPaymentCollections } from "../../__fixtures__"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("Payment Module Service", () => {
|
||||
let service: IPaymentModuleService
|
||||
let repositoryManager: SqlEntityManager
|
||||
let shutdownFunc: () => Promise<void>
|
||||
|
||||
beforeAll(async () => {
|
||||
await MikroOrmWrapper.setupDatabase()
|
||||
|
||||
const initModulesConfig = getInitModuleConfig()
|
||||
const { medusaApp, shutdown } = await initModules(initModulesConfig)
|
||||
service = medusaApp.modules[Modules.PAYMENT]
|
||||
|
||||
shutdownFunc = shutdown
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await shutdownFunc()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await MikroOrmWrapper.setupDatabase()
|
||||
repositoryManager = await MikroOrmWrapper.forkManager()
|
||||
|
||||
await createPaymentCollections(repositoryManager)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await MikroOrmWrapper.clearDatabase()
|
||||
})
|
||||
|
||||
describe("providers", () => {
|
||||
it("should load payment plugins", async () => {
|
||||
let error = await service
|
||||
.createPaymentCollections([
|
||||
{
|
||||
amount: 200,
|
||||
region_id: "req_123",
|
||||
} as any,
|
||||
])
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toContain(
|
||||
"Value for PaymentCollection.currency_code is required, 'undefined' found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a payment collection successfully", async () => {
|
||||
const [createdPaymentCollection] = await service.createPaymentCollections(
|
||||
[{ currency_code: "USD", amount: 200, region_id: "reg_123" }]
|
||||
)
|
||||
|
||||
expect(createdPaymentCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
status: "not_paid",
|
||||
payment_providers: [],
|
||||
payment_sessions: [],
|
||||
payments: [],
|
||||
currency_code: "USD",
|
||||
amount: 200,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -39,7 +39,7 @@ moduleIntegrationTestRunner({
|
||||
const paymentSession = await service.createPaymentSession(
|
||||
paymentCollection.id,
|
||||
{
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
providerContext: {
|
||||
amount: 200,
|
||||
currency_code: "USD",
|
||||
@@ -48,7 +48,6 @@ moduleIntegrationTestRunner({
|
||||
customer: {},
|
||||
billing_address: {},
|
||||
email: "test@test.test.com",
|
||||
resource_id: "cart_test",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -86,7 +85,7 @@ moduleIntegrationTestRunner({
|
||||
id: expect.any(String),
|
||||
currency_code: "USD",
|
||||
amount: 200,
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
status: "authorized",
|
||||
authorized_at: expect.any(Date),
|
||||
}),
|
||||
@@ -96,7 +95,7 @@ moduleIntegrationTestRunner({
|
||||
id: expect.any(String),
|
||||
amount: 200,
|
||||
currency_code: "USD",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
amount: 200,
|
||||
@@ -335,7 +334,7 @@ moduleIntegrationTestRunner({
|
||||
describe("create", () => {
|
||||
it("should create a payment session successfully", async () => {
|
||||
await service.createPaymentSession("pay-col-id-1", {
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
providerContext: {
|
||||
amount: 200,
|
||||
currency_code: "usd",
|
||||
@@ -365,7 +364,7 @@ moduleIntegrationTestRunner({
|
||||
authorized_at: null,
|
||||
currency_code: "usd",
|
||||
amount: 200,
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@@ -376,7 +375,7 @@ moduleIntegrationTestRunner({
|
||||
describe("update", () => {
|
||||
it("should update a payment session successfully", async () => {
|
||||
let session = await service.createPaymentSession("pay-col-id-1", {
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
providerContext: {
|
||||
amount: 200,
|
||||
currency_code: "usd",
|
||||
@@ -423,7 +422,7 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
const session = await service.createPaymentSession(collection.id, {
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
providerContext: {
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
@@ -445,9 +444,8 @@ moduleIntegrationTestRunner({
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 100,
|
||||
authorized_amount: 100,
|
||||
currency_code: "usd",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
|
||||
refunds: [],
|
||||
captures: [],
|
||||
@@ -467,7 +465,7 @@ moduleIntegrationTestRunner({
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
raw_amount: { value: "100", precision: 20 },
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
data: {},
|
||||
status: "authorized",
|
||||
authorized_at: expect.any(Date),
|
||||
@@ -475,7 +473,6 @@ moduleIntegrationTestRunner({
|
||||
id: expect.any(String),
|
||||
}),
|
||||
payment: expect.objectContaining({
|
||||
authorized_amount: 100,
|
||||
cart_id: null,
|
||||
order_id: null,
|
||||
order_edit_id: null,
|
||||
@@ -488,7 +485,7 @@ moduleIntegrationTestRunner({
|
||||
captures: [],
|
||||
amount: 100,
|
||||
currency_code: "usd",
|
||||
provider_id: "system",
|
||||
provider_id: "pp_system_default",
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,6 +10,21 @@ export function getInitModuleConfig() {
|
||||
schema: process.env.MEDUSA_PAYMENT_DB_SCHEMA,
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
resolve: "@medusajs/payment-stripe",
|
||||
options: {
|
||||
config: {
|
||||
dkk: {
|
||||
apiKey: "pk_test_123",
|
||||
},
|
||||
usd: {
|
||||
apiKey: "pk_test_456",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const injectedDependencies = {}
|
||||
|
||||
@@ -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,
|
||||
| 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) => {
|
||||
Object.entries(pluginOptions.config || []).map(([name, config]) => {
|
||||
const key = `pp_${klass.PROVIDER}_${name}`
|
||||
|
||||
container.register({
|
||||
[`pp_${klass.identifier}`]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginOptions),
|
||||
{
|
||||
[key]: asFunction((cradle) => new klass(cradle, config), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
container.registerAdd(
|
||||
"payment_providers",
|
||||
asFunction((cradle) => new klass(cradle, pluginOptions), {
|
||||
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,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
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,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Logger } from "../logger"
|
||||
export type Constructor<T> = new (...args: any[]) => T
|
||||
export * from "../common/medusa-container"
|
||||
export * from "./internal-module-service"
|
||||
export * from "./module-provider"
|
||||
|
||||
export type LogLevel =
|
||||
| "query"
|
||||
@@ -290,13 +291,3 @@ export interface IModuleService {
|
||||
onApplicationStart?: () => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
export type ModuleProviderExports = {
|
||||
services: Constructor<any>[]
|
||||
}
|
||||
|
||||
export type ModuleProvider = {
|
||||
resolve: string | ModuleProviderExports
|
||||
provider_name?: string
|
||||
options: Record<string, unknown>
|
||||
}
|
||||
|
||||
11
packages/types/src/modules-sdk/module-provider.ts
Normal file
11
packages/types/src/modules-sdk/module-provider.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Constructor } from "./index"
|
||||
|
||||
export type ModuleProviderExports = {
|
||||
services: Constructor<any>[]
|
||||
}
|
||||
|
||||
export type ModuleProvider = {
|
||||
resolve: string | ModuleProviderExports
|
||||
provider_name?: string
|
||||
options: Record<string, unknown>
|
||||
}
|
||||
@@ -280,6 +280,23 @@ export interface PaymentDTO {
|
||||
payment_session?: PaymentSessionDTO
|
||||
}
|
||||
|
||||
export interface FilterablePaymentProps
|
||||
extends BaseFilterable<FilterablePaymentProps> {
|
||||
id?: string | string[]
|
||||
|
||||
session_id?: string | string[] | OperatorMap<string>
|
||||
|
||||
customer_id?: string | string[] | OperatorMap<string>
|
||||
cart_id?: string | string[] | OperatorMap<string>
|
||||
order_id?: string | string[] | OperatorMap<string>
|
||||
order_edit_id?: string | string[] | OperatorMap<string>
|
||||
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
captured_at?: OperatorMap<string>
|
||||
canceled_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* The capture details.
|
||||
*/
|
||||
|
||||
@@ -188,7 +188,7 @@ export interface CreatePaymentSessionDTO {
|
||||
/**
|
||||
* The provider's context.
|
||||
*/
|
||||
providerContext: PaymentProviderContext
|
||||
providerContext: Omit<PaymentProviderContext, "resource_id">
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,3 +218,21 @@ export interface CreatePaymentProviderDTO {
|
||||
*/
|
||||
is_enabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook
|
||||
*/
|
||||
export interface ProviderWebhookPayload {
|
||||
provider: string
|
||||
payload: {
|
||||
/**
|
||||
* Parsed webhook body
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
/**
|
||||
* Raw request body
|
||||
*/
|
||||
rawData: string | Buffer
|
||||
headers: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,33 @@
|
||||
import { PaymentSessionStatus } from "./common"
|
||||
import { CustomerDTO } from "../customer"
|
||||
import { AddressDTO } from "../address"
|
||||
import { ProviderWebhookPayload } from "./mutations"
|
||||
|
||||
export type PaymentAddressDTO = Partial<AddressDTO>
|
||||
|
||||
export type PaymentCustomerDTO = Partial<CustomerDTO>
|
||||
|
||||
/**
|
||||
* Normalized events from payment provider to internal payment module events.
|
||||
*/
|
||||
export enum PaymentActions {
|
||||
/**
|
||||
* Payment session has been authorized and there are available funds for capture.
|
||||
*/
|
||||
AUTHORIZED = "authorized",
|
||||
/**
|
||||
* Payment was successful and the mount is captured.
|
||||
*/
|
||||
SUCCESSFUL = "captured",
|
||||
/**
|
||||
* Payment failed.
|
||||
*/
|
||||
FAILED = "failed",
|
||||
/**
|
||||
* Received an event that is not processable.
|
||||
*/
|
||||
NOT_SUPPORTED = "not_supported",
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
@@ -9,7 +38,7 @@ export type PaymentProviderContext = {
|
||||
/**
|
||||
* The payment's billing address.
|
||||
*/
|
||||
billing_address?: Record<string, unknown> | null // TODO: revisit types
|
||||
billing_address?: PaymentAddressDTO
|
||||
/**
|
||||
* The customer's email.
|
||||
*/
|
||||
@@ -23,17 +52,17 @@ export type PaymentProviderContext = {
|
||||
*/
|
||||
amount: number
|
||||
/**
|
||||
* The ID of the resource the payment is associated with. For example, the cart's ID.
|
||||
* The ID of the resource the payment is associated with i.e. the ID of the PaymentSession in Medusa
|
||||
*/
|
||||
resource_id: string
|
||||
/**
|
||||
* The customer associated with this payment.
|
||||
*/
|
||||
customer?: Record<string, unknown> // TODO: type
|
||||
customer?: PaymentCustomerDTO
|
||||
/**
|
||||
* The context.
|
||||
*/
|
||||
context: Record<string, unknown>
|
||||
context: { payment_description?: string } & Record<string, unknown>
|
||||
/**
|
||||
* If the payment session hasn't been created or initiated yet, it'll be an empty object.
|
||||
* If the payment session exists, it'll be the value of the payment session's `data` field.
|
||||
@@ -88,6 +117,20 @@ export interface PaymentProviderError {
|
||||
detail?: any
|
||||
}
|
||||
|
||||
export type WebhookActionData = {
|
||||
resource_id: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
export type WebhookActionResult =
|
||||
| {
|
||||
action: PaymentActions.NOT_SUPPORTED
|
||||
}
|
||||
| {
|
||||
action: PaymentActions
|
||||
data: WebhookActionData
|
||||
}
|
||||
|
||||
export interface IPaymentProvider {
|
||||
/**
|
||||
* @ignore
|
||||
@@ -209,4 +252,15 @@ export interface IPaymentProvider {
|
||||
getPaymentStatus(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentSessionStatus>
|
||||
|
||||
/**
|
||||
* The method is called when å webhook call for this particular provider is received.
|
||||
*
|
||||
* The method is responsible for normalizing the received event and provide
|
||||
*
|
||||
* @param data - object containing provider id and data from the provider
|
||||
*/
|
||||
getWebhookActionAndData(
|
||||
data: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult>
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { FindConfig } from "../common"
|
||||
import { IModuleService } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
import {
|
||||
CreateCaptureDTO,
|
||||
CreatePaymentCollectionDTO,
|
||||
CreatePaymentDTO,
|
||||
CreatePaymentSessionDTO,
|
||||
CreateRefundDTO,
|
||||
UpdatePaymentCollectionDTO,
|
||||
UpdatePaymentDTO,
|
||||
UpdatePaymentSessionDTO,
|
||||
} from "./mutations"
|
||||
import {
|
||||
FilterablePaymentCollectionProps,
|
||||
FilterablePaymentProps,
|
||||
PaymentCollectionDTO,
|
||||
PaymentDTO,
|
||||
PaymentSessionDTO,
|
||||
} from "./common"
|
||||
import { FindConfig } from "../common"
|
||||
import {
|
||||
CreateCaptureDTO,
|
||||
CreatePaymentCollectionDTO,
|
||||
CreatePaymentSessionDTO,
|
||||
CreateRefundDTO,
|
||||
ProviderWebhookPayload,
|
||||
UpdatePaymentCollectionDTO,
|
||||
UpdatePaymentDTO,
|
||||
UpdatePaymentSessionDTO,
|
||||
} from "./mutations"
|
||||
|
||||
/**
|
||||
* The main service interface for the payment module.
|
||||
@@ -311,6 +312,24 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
|
||||
/* ********** PAYMENT ********** */
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of payments based on optional filters and configuration.
|
||||
*
|
||||
* @param {FilterablePaymentProps} filters - The filters to apply on the retrieved payment.
|
||||
* @param {FindConfig<PaymentDTO>} config - The configurations determining how the payment is retrieved. Its properties, such as `select` or `relations`, accept the
|
||||
* attributes or relations associated with a payment.
|
||||
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
|
||||
* @returns {Promise<PaymentDTO[]>} A list of payment.
|
||||
*
|
||||
* @example
|
||||
* {example-code}
|
||||
*/
|
||||
listPayments(
|
||||
filters?: FilterablePaymentProps,
|
||||
config?: FindConfig<PaymentDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentDTO[]>
|
||||
|
||||
/**
|
||||
* This method updates an existing payment.
|
||||
*
|
||||
@@ -375,4 +394,21 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
* {example-code}
|
||||
*/
|
||||
createProvidersOnLoad(): Promise<void>
|
||||
|
||||
/* ********** HOOKS ********** */
|
||||
|
||||
processEvent(data: ProviderWebhookPayload): Promise<void>
|
||||
}
|
||||
|
||||
export interface PaymentModuleOptions {
|
||||
/**
|
||||
* The delay in milliseconds before processing the webhook event.
|
||||
* @defaultValue 5000
|
||||
*/
|
||||
webhook_delay?: number
|
||||
/**
|
||||
* The number of times to retry the webhook event processing in case of an error.
|
||||
* @defaultValue 3
|
||||
*/
|
||||
webhook_retries?: number
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import {
|
||||
PaymentProviderError,
|
||||
PaymentProviderSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
ProviderWebhookPayload,
|
||||
WebhookActionResult,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export abstract class AbstractPaymentProvider implements IPaymentProvider {
|
||||
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
|
||||
implements IPaymentProvider
|
||||
{
|
||||
/**
|
||||
* You can use the `constructor` of your Payment Provider to have access to different services in Medusa through [dependency injection](https://docs.medusajs.com/development/fundamentals/dependency-injection).
|
||||
*
|
||||
@@ -38,7 +42,7 @@ export abstract class AbstractPaymentProvider implements IPaymentProvider {
|
||||
*/
|
||||
protected constructor(
|
||||
protected readonly container: MedusaContainer,
|
||||
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
protected readonly config: TConfig = {} as TConfig // eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -126,8 +130,18 @@ export abstract class AbstractPaymentProvider implements IPaymentProvider {
|
||||
abstract updatePayment(
|
||||
context: PaymentProviderContext
|
||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
||||
|
||||
abstract getWebhookActionAndData(
|
||||
data: ProviderWebhookPayload["payload"]
|
||||
): Promise<WebhookActionResult>
|
||||
}
|
||||
|
||||
export function isPaymentProviderError(obj: any): obj is PaymentProviderError {
|
||||
return obj && typeof obj === "object" && obj.error && obj.code && obj.detail
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === "object" &&
|
||||
"error" in obj &&
|
||||
"code" in obj &&
|
||||
"detail" in obj
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./abstract-payment-provider"
|
||||
export * from "./payment-collection"
|
||||
export * from "./payment-session"
|
||||
export * from "./abstract-payment-provider"
|
||||
export * from "./webhook"
|
||||
|
||||
25
packages/utils/src/payment/webhook.ts
Normal file
25
packages/utils/src/payment/webhook.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export enum PaymentWebhookEvents {
|
||||
WebhookReceived = "payment.webhook_received",
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized events from payment provider to internal payment module events.
|
||||
*/
|
||||
export enum PaymentActions {
|
||||
/**
|
||||
* Payment session has been authorized and there are available funds for capture.
|
||||
*/
|
||||
AUTHORIZED = "authorized",
|
||||
/**
|
||||
* Payment was successful and the mount is captured.
|
||||
*/
|
||||
SUCCESSFUL = "captured",
|
||||
/**
|
||||
* Payment failed.
|
||||
*/
|
||||
FAILED = "failed",
|
||||
/**
|
||||
* Received an event that is not processable.
|
||||
*/
|
||||
NOT_SUPPORTED = "not_supported",
|
||||
}
|
||||
30
yarn.lock
30
yarn.lock
@@ -8550,6 +8550,26 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/payment-stripe@workspace:packages/payment-stripe":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/payment-stripe@workspace:packages/payment-stripe"
|
||||
dependencies:
|
||||
"@medusajs/medusa": ^1.19.1
|
||||
"@medusajs/utils": ^1.11.3
|
||||
"@types/stripe": ^8.0.417
|
||||
awilix: ^8.0.1
|
||||
body-parser: ^1.19.0
|
||||
cross-env: ^5.2.1
|
||||
express: ^4.17.1
|
||||
jest: ^25.5.4
|
||||
rimraf: ^5.0.1
|
||||
stripe: latest
|
||||
typescript: ^4.9.5
|
||||
peerDependencies:
|
||||
"@medusajs/medusa": ^1.12.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/payment@workspace:packages/payment":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/payment@workspace:packages/payment"
|
||||
@@ -47770,6 +47790,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:latest":
|
||||
version: 14.16.0
|
||||
resolution: "stripe@npm:14.16.0"
|
||||
dependencies:
|
||||
"@types/node": ">=8.1.0"
|
||||
qs: ^6.11.0
|
||||
checksum: bada06609592bae71094ba86fdf745d86945d6bb5b44482da0355235c01ca6f2c76f261fad31d9d367cee5cf6b8b5532fb66733d664d4bb497125809b61699bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strnum@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "strnum@npm:1.0.5"
|
||||
|
||||
Reference in New Issue
Block a user