feat(payment, payment-stripe): Add Stripe module provider (#6311)

This commit is contained in:
Oli Juhl
2024-02-26 19:48:15 +01:00
committed by GitHub
parent ac829fc67f
commit ce39b9b66e
49 changed files with 1256 additions and 119 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(payment-stripe): new Stripe payment provider

View File

@@ -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"

View File

@@ -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
})
}
}

View File

@@ -0,0 +1,9 @@
import { MiddlewareRoute } from "../../types/middlewares"
export const hooksRoutesMiddlewares: MiddlewareRoute[] = [
{
method: ["POST"],
bodyParser: { preserveRawBody: true },
matcher: "/hooks/payment/:provider",
},
]

View 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)
}

View File

@@ -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,
],
}

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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
View File

@@ -0,0 +1,4 @@
dist
node_modules
.DS_store
yarn.lock

View File

View File

View 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`],
}

View 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"
]
}

View 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

View 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

View 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"

View 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

View 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

View 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

View 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

View 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

View 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

View 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",
}

View 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"
]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -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: {},
},
]

View File

@@ -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,
})
)
})
})
})

View File

@@ -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",
}),
},
})

View File

@@ -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 = {}

View File

@@ -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>({

View File

@@ -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({

View File

@@ -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 **/

View File

@@ -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

View File

@@ -1,2 +1,3 @@
export { default as PaymentModuleService } from "./payment-module"
export { default as PaymentProviderService } from "./payment-provider"

View File

@@ -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)

View File

@@ -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,

View File

@@ -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>
}

View 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>
}

View File

@@ -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.
*/

View File

@@ -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>
}
}

View File

@@ -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>
}

View File

@@ -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
}

View File

@@ -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
)
}

View File

@@ -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"

View 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",
}

View File

@@ -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"