feat: Create payment sessions (#6549)
~~Opening a draft PR to discuss a couple of implementation details that we should align on~~ **What** Add workflow and API endpoint for creating payment sessions for a payment collection. Endpoint is currently `POST /store/payment-collection/:id/payment-sessions`. I suggested an alternative in a comment below. Please note, we intentionally do not want to support creating payment sessions in bulk, as this would become a mess when having to manage multiple calls to third-party providers.
This commit is contained in:
1 parent
908b1dc3a2
commit
84208aafc1
30 files changed
+603
-182
No files matched your search
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
createPaymentSessionsWorkflow,
|
||||||
|
createPaymentSessionsWorkflowId,
|
||||||
|
} from "@medusajs/core-flows"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
|
||||||
|
import path from "path"
|
||||||
|
import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app"
|
||||||
|
import { getContainer } from "../../../environment-helpers/use-container"
|
||||||
|
import { initDb, useDb } from "../../../environment-helpers/use-db"
|
||||||
|
|
||||||
|
jest.setTimeout(50000)
|
||||||
|
|
||||||
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
|
|
||||||
|
describe("Carts workflows", () => {
|
||||||
|
let dbConnection
|
||||||
|
let appContainer
|
||||||
|
let shutdownServer
|
||||||
|
let paymentModule: IPaymentModuleService
|
||||||
|
let regionModule: IRegionModuleService
|
||||||
|
let remoteLink
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
|
dbConnection = await initDb({ cwd, env } as any)
|
||||||
|
shutdownServer = await startBootstrapApp({ cwd, env })
|
||||||
|
appContainer = getContainer()
|
||||||
|
paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT)
|
||||||
|
regionModule = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||||
|
remoteLink = appContainer.resolve("remoteLink")
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
await shutdownServer()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.teardown()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createPaymentSessionWorkflow", () => {
|
||||||
|
it("should create payment sessions", async () => {
|
||||||
|
const region = await regionModule.create({
|
||||||
|
currency_code: "usd",
|
||||||
|
name: "US",
|
||||||
|
})
|
||||||
|
|
||||||
|
let paymentCollection = await paymentModule.createPaymentCollections({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
region_id: region.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await createPaymentSessionsWorkflow(appContainer).run({
|
||||||
|
input: {
|
||||||
|
payment_collection_id: paymentCollection.id,
|
||||||
|
provider_id: "pp_system_default",
|
||||||
|
context: {},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
paymentCollection = await paymentModule.retrievePaymentCollection(
|
||||||
|
paymentCollection.id,
|
||||||
|
{
|
||||||
|
relations: ["payment_sessions"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(paymentCollection).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: paymentCollection.id,
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
region_id: region.id,
|
||||||
|
payment_sessions: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 1000,
|
||||||
|
currency_code: "usd",
|
||||||
|
provider_id: "pp_system_default",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("compensation", () => {
|
||||||
|
it("should delete created payment collection if a subsequent step fails", async () => {
|
||||||
|
const workflow = createPaymentSessionsWorkflow(appContainer)
|
||||||
|
|
||||||
|
workflow.appendAction("throw", createPaymentSessionsWorkflowId, {
|
||||||
|
invoke: async function failStep() {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to do something after creating payment sessions`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const region = await regionModule.create({
|
||||||
|
currency_code: "usd",
|
||||||
|
name: "US",
|
||||||
|
})
|
||||||
|
|
||||||
|
let paymentCollection = await paymentModule.createPaymentCollections({
|
||||||
|
currency_code: "usd",
|
||||||
|
amount: 1000,
|
||||||
|
region_id: region.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { errors } = await workflow.run({
|
||||||
|
input: {
|
||||||
|
payment_collection_id: paymentCollection.id,
|
||||||
|
provider_id: "pp_system_default",
|
||||||
|
context: {},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "throw",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: new Error(
|
||||||
|
`Failed to do something after creating payment sessions`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const sessions = await paymentModule.listPaymentSessions({
|
||||||
|
payment_collection_id: paymentCollection.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(sessions).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from "./cart"
|
export * from "./cart"
|
||||||
export * from "./inventory"
|
export * from "./inventory"
|
||||||
export * from "./line-item"
|
export * from "./line-item"
|
||||||
|
export * from "./payment-collection"
|
||||||
export * from "./price-list"
|
export * from "./price-list"
|
||||||
export * from "./product"
|
export * from "./product"
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./steps"
|
||||||
|
export * from "./workflows"
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import { IPaymentModuleService, PaymentProviderContext } from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
interface StepInput {
|
||||||
|
payment_collection_id: string
|
||||||
|
provider_id: string
|
||||||
|
amount: number
|
||||||
|
currency_code: string
|
||||||
|
context?: PaymentProviderContext
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPaymentSessionStepId = "create-payment-session"
|
||||||
|
export const createPaymentSessionStep = createStep(
|
||||||
|
createPaymentSessionStepId,
|
||||||
|
async (input: StepInput, { container }) => {
|
||||||
|
const service = container.resolve<IPaymentModuleService>(
|
||||||
|
ModuleRegistrationName.PAYMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
const session = await service.createPaymentSession(
|
||||||
|
input.payment_collection_id,
|
||||||
|
{
|
||||||
|
provider_id: input.provider_id,
|
||||||
|
currency_code: input.currency_code,
|
||||||
|
amount: input.amount,
|
||||||
|
data: input.data ?? {},
|
||||||
|
context: input.context,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return new StepResponse(session, session.id)
|
||||||
|
},
|
||||||
|
async (createdSession, { container }) => {
|
||||||
|
if (!createdSession) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IPaymentModuleService>(
|
||||||
|
ModuleRegistrationName.PAYMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.deletePaymentSession(createdSession)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./create-payment-session"
|
||||||
|
export * from "./retrieve-payment-collection"
|
||||||
|
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import {
|
||||||
|
FindConfig,
|
||||||
|
IPaymentModuleService,
|
||||||
|
PaymentCollectionDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
interface StepInput {
|
||||||
|
id: string
|
||||||
|
config?: FindConfig<PaymentCollectionDTO>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const retrievePaymentCollectionStepId = "retrieve-payment-collection"
|
||||||
|
export const retrievePaymentCollectionStep = createStep(
|
||||||
|
retrievePaymentCollectionStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
const paymentModuleService = container.resolve<IPaymentModuleService>(
|
||||||
|
ModuleRegistrationName.PAYMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
const paymentCollection =
|
||||||
|
await paymentModuleService.retrievePaymentCollection(data.id, data.config)
|
||||||
|
|
||||||
|
return new StepResponse(paymentCollection)
|
||||||
|
}
|
||||||
|
)
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import { PaymentProviderContext, PaymentSessionDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import {
|
||||||
|
createPaymentSessionStep,
|
||||||
|
retrievePaymentCollectionStep,
|
||||||
|
} from "../steps"
|
||||||
|
|
||||||
|
interface WorkflowInput {
|
||||||
|
payment_collection_id: string
|
||||||
|
provider_id: string
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
context?: PaymentProviderContext
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPaymentSessionsWorkflowId = "create-payment-sessions"
|
||||||
|
export const createPaymentSessionsWorkflow = createWorkflow(
|
||||||
|
createPaymentSessionsWorkflowId,
|
||||||
|
(input: WorkflowData<WorkflowInput>): WorkflowData<PaymentSessionDTO> => {
|
||||||
|
const paymentCollection = retrievePaymentCollectionStep({
|
||||||
|
id: input.payment_collection_id,
|
||||||
|
config: {
|
||||||
|
select: ["id", "amount", "currency_code"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const paymentSessionInput = transform(
|
||||||
|
{ paymentCollection, input },
|
||||||
|
(data) => {
|
||||||
|
return {
|
||||||
|
payment_collection_id: data.input.payment_collection_id,
|
||||||
|
provider_id: data.input.provider_id,
|
||||||
|
data: data.input.data,
|
||||||
|
context: data.input.context,
|
||||||
|
amount: data.paymentCollection.amount,
|
||||||
|
currency_code: data.paymentCollection.currency_code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const created = createPaymentSessionStep(paymentSessionInput)
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-payment-session"
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
|
import { createCartWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { CreateCartWorkflowInputDTO } from "@medusajs/types"
|
||||||
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
AuthenticatedMedusaRequest,
|
AuthenticatedMedusaRequest,
|
||||||
MedusaResponse,
|
MedusaResponse,
|
||||||
} from "../../../types/routing"
|
} from "../../../types/routing"
|
||||||
|
|
||||||
import { createCartWorkflow } from "@medusajs/core-flows"
|
|
||||||
import { CreateCartWorkflowInputDTO } from "@medusajs/types"
|
|
||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
|
||||||
import { defaultStoreCartFields } from "../carts/query-config"
|
import { defaultStoreCartFields } from "../carts/query-config"
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: AuthenticatedMedusaRequest<CreateCartWorkflowInputDTO>,
|
req: AuthenticatedMedusaRequest<CreateCartWorkflowInputDTO>,
|
||||||
res: MedusaResponse
|
res: MedusaResponse
|
||||||
) => {
|
) => {
|
||||||
const workflowInput = req.validatedBody
|
const workflowInput = {
|
||||||
|
...req.validatedBody,
|
||||||
// If the customer is logged in, we auto-assign them to the cart
|
customer_id: req.auth?.actor_id,
|
||||||
if (req.auth?.actor_id) {
|
|
||||||
workflowInput.customer_id = req.auth.actor_id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result, errors } = await createCartWorkflow(req.scope).run({
|
const { result, errors } = await createCartWorkflow(req.scope).run({
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import { transformBody } from "../../../../../api/middlewares"
|
||||||
|
import { MiddlewareRoute } from "../../../../../loaders/helpers/routing/types"
|
||||||
|
import { StorePostPaymentCollectionsPaymentSessionReq } from "./validators"
|
||||||
|
|
||||||
|
export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/store/payment-collections/:id/payment-sessions",
|
||||||
|
middlewares: [transformBody(StorePostPaymentCollectionsPaymentSessionReq)],
|
||||||
|
},
|
||||||
|
]
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
export const defaultStorePaymentCollectionFields = [
|
||||||
|
"id",
|
||||||
|
"currency_code",
|
||||||
|
"amount",
|
||||||
|
"payment_sessions",
|
||||||
|
"payment_sessions.id",
|
||||||
|
"payment_sessions.amount",
|
||||||
|
"payment_sessions.provider_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const defaultStorePaymentCollectionRelations = ["payment_sessions"]
|
||||||
|
|
||||||
|
export const allowedStorePaymentCollectionRelations = ["payment_sessions"]
|
||||||
|
|
||||||
|
export const retrieveTransformQueryConfig = {
|
||||||
|
defaultFields: defaultStorePaymentCollectionFields,
|
||||||
|
defaultRelations: defaultStorePaymentCollectionRelations,
|
||||||
|
allowedRelations: allowedStorePaymentCollectionRelations,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { createPaymentSessionsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../../types/routing"
|
||||||
|
import { defaultStorePaymentCollectionFields } from "./query-config"
|
||||||
|
import { StorePostPaymentCollectionsPaymentSessionReq } from "./validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: AuthenticatedMedusaRequest<StorePostPaymentCollectionsPaymentSessionReq>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { context, provider_id, data } = req.body
|
||||||
|
|
||||||
|
// If the customer is logged in, we auto-assign them to the payment collection
|
||||||
|
if (req.auth?.actor_id) {
|
||||||
|
context.customer = {
|
||||||
|
...context.customer,
|
||||||
|
id: req.auth.actor_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowInput = {
|
||||||
|
payment_collection_id: id,
|
||||||
|
provider_id: provider_id,
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = await createPaymentSessionsWorkflow(req.scope).run({
|
||||||
|
input: workflowInput as any,
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(errors) && errors[0]) {
|
||||||
|
throw errors[0].error
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteQuery = req.scope.resolve("remoteQuery")
|
||||||
|
|
||||||
|
const query = remoteQueryObjectFromString({
|
||||||
|
entryPoint: "payment_collection",
|
||||||
|
variables: { cart: { id } },
|
||||||
|
fields: defaultStorePaymentCollectionFields,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [result] = await remoteQuery(query)
|
||||||
|
|
||||||
|
res.status(200).json({ payment_collection: result })
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import { PaymentProviderContext } from "@medusajs/types"
|
||||||
|
|
||||||
|
export class StorePostPaymentCollectionsPaymentSessionReq {
|
||||||
|
provider_id: string
|
||||||
|
context?: PaymentProviderContext
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
}
|
||||||
@@ -4,21 +4,22 @@ import Stripe from "stripe"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
MedusaContainer,
|
MedusaContainer,
|
||||||
PaymentSessionStatus,
|
|
||||||
PaymentProviderContext,
|
|
||||||
PaymentProviderError,
|
PaymentProviderError,
|
||||||
PaymentProviderSessionResponse,
|
PaymentProviderSessionResponse,
|
||||||
|
PaymentSessionStatus,
|
||||||
ProviderWebhookPayload,
|
ProviderWebhookPayload,
|
||||||
|
UpdatePaymentProviderSession,
|
||||||
WebhookActionResult,
|
WebhookActionResult,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
PaymentActions,
|
|
||||||
AbstractPaymentProvider,
|
AbstractPaymentProvider,
|
||||||
isPaymentProviderError,
|
|
||||||
MedusaError,
|
MedusaError,
|
||||||
|
PaymentActions,
|
||||||
|
isPaymentProviderError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { isDefined } from "medusa-core-utils"
|
import { isDefined } from "medusa-core-utils"
|
||||||
|
|
||||||
|
import { CreatePaymentProviderSession } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
ErrorIntentStatus,
|
ErrorIntentStatus,
|
||||||
@@ -103,26 +104,20 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initiatePayment(
|
async initiatePayment(
|
||||||
context: PaymentProviderContext
|
input: CreatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||||
const intentRequestData = this.getPaymentIntentOptions()
|
const intentRequestData = this.getPaymentIntentOptions()
|
||||||
const {
|
const { email, extra, resource_id, customer } = input.context
|
||||||
email,
|
const { currency_code, amount } = input
|
||||||
context: cart_context,
|
|
||||||
currency_code,
|
|
||||||
amount,
|
|
||||||
resource_id,
|
|
||||||
customer,
|
|
||||||
} = context
|
|
||||||
|
|
||||||
const description = (cart_context.payment_description ??
|
const description = (extra?.payment_description ??
|
||||||
this.options_?.payment_description) as string
|
this.options_?.payment_description) as string
|
||||||
|
|
||||||
const intentRequest: Stripe.PaymentIntentCreateParams = {
|
const intentRequest: Stripe.PaymentIntentCreateParams = {
|
||||||
description,
|
description,
|
||||||
amount: Math.round(amount),
|
amount: Math.round(amount),
|
||||||
currency: currency_code,
|
currency: currency_code,
|
||||||
metadata: { resource_id },
|
metadata: { resource_id: resource_id ?? "Medusa Payment" },
|
||||||
capture_method: this.options_.capture ? "automatic" : "manual",
|
capture_method: this.options_.capture ? "automatic" : "manual",
|
||||||
...intentRequestData,
|
...intentRequestData,
|
||||||
}
|
}
|
||||||
@@ -149,9 +144,9 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
|||||||
intentRequest.customer = stripeCustomer.id
|
intentRequest.customer = stripeCustomer.id
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_data
|
let sessionData
|
||||||
try {
|
try {
|
||||||
session_data = (await this.stripe_.paymentIntents.create(
|
sessionData = (await this.stripe_.paymentIntents.create(
|
||||||
intentRequest
|
intentRequest
|
||||||
)) as unknown as Record<string, unknown>
|
)) as unknown as Record<string, unknown>
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -162,7 +157,7 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: session_data,
|
data: sessionData,
|
||||||
// TODO: REVISIT
|
// TODO: REVISIT
|
||||||
// update_requests: customer?.metadata?.stripe_id
|
// update_requests: customer?.metadata?.stripe_id
|
||||||
// ? undefined
|
// ? undefined
|
||||||
@@ -260,13 +255,14 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updatePayment(
|
async updatePayment(
|
||||||
context: PaymentProviderContext
|
input: UpdatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
|
||||||
const { amount, customer, payment_session_data } = context
|
const { context, data, amount } = input
|
||||||
const stripeId = customer?.metadata?.stripe_id
|
|
||||||
|
|
||||||
if (stripeId !== payment_session_data.customer) {
|
const stripeId = context.customer?.metadata?.stripe_id
|
||||||
const result = await this.initiatePayment(context)
|
|
||||||
|
if (stripeId !== data.customer) {
|
||||||
|
const result = await this.initiatePayment(input)
|
||||||
if (isPaymentProviderError(result)) {
|
if (isPaymentProviderError(result)) {
|
||||||
return this.buildError(
|
return this.buildError(
|
||||||
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
|
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
|
||||||
@@ -276,12 +272,12 @@ abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
if (amount && payment_session_data.amount === Math.round(amount)) {
|
if (amount && data.amount === Math.round(amount)) {
|
||||||
return { data: payment_session_data }
|
return { data }
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const id = payment_session_data.id as string
|
const id = data.id as string
|
||||||
const sessionData = (await this.stripe_.paymentIntents.update(id, {
|
const sessionData = (await this.stripe_.paymentIntents.update(id, {
|
||||||
amount: Math.round(amount),
|
amount: Math.round(amount),
|
||||||
})) as unknown as PaymentProviderSessionResponse["data"]
|
})) as unknown as PaymentProviderSessionResponse["data"]
|
||||||
|
|||||||
@@ -25,21 +25,21 @@ export const defaultPaymentSessionData = [
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
payment_collection: "pay-col-id-1",
|
payment_collection_id: "pay-col-id-1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pay-sess-id-2",
|
id: "pay-sess-id-2",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
payment_collection: "pay-col-id-2",
|
payment_collection_id: "pay-col-id-2",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pay-sess-id-3",
|
id: "pay-sess-id-3",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
payment_collection: "pay-col-id-2",
|
payment_collection_id: "pay-col-id-2",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export const defaultPaymentData = [
|
|||||||
id: "pay-id-1",
|
id: "pay-id-1",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
payment_collection: "pay-col-id-1",
|
payment_collection_id: "pay-col-id-1",
|
||||||
payment_session: "pay-sess-id-1",
|
payment_session: "pay-sess-id-1",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
authorized_amount: 100,
|
authorized_amount: 100,
|
||||||
@@ -59,7 +59,7 @@ export const defaultPaymentData = [
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
authorized_amount: 100,
|
authorized_amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
payment_collection: "pay-col-id-2",
|
payment_collection_id: "pay-col-id-2",
|
||||||
payment_session: "pay-sess-id-2",
|
payment_session: "pay-sess-id-2",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
data: {},
|
data: {},
|
||||||
|
|||||||
+34
-41
@@ -21,17 +21,9 @@ moduleIntegrationTestRunner({
|
|||||||
}: SuiteOptions<IPaymentModuleService>) => {
|
}: SuiteOptions<IPaymentModuleService>) => {
|
||||||
describe("Payment Module Service", () => {
|
describe("Payment Module Service", () => {
|
||||||
describe("Payment Flow", () => {
|
describe("Payment Flow", () => {
|
||||||
beforeEach(async () => {
|
|
||||||
const repositoryManager = MikroOrmWrapper.forkManager()
|
|
||||||
|
|
||||||
await createPaymentCollections(repositoryManager)
|
|
||||||
await createPaymentSessions(repositoryManager)
|
|
||||||
await createPayments(repositoryManager)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("complete payment flow successfully", async () => {
|
it("complete payment flow successfully", async () => {
|
||||||
let paymentCollection = await service.createPaymentCollections({
|
let paymentCollection = await service.createPaymentCollections({
|
||||||
currency_code: "USD",
|
currency_code: "usd",
|
||||||
amount: 200,
|
amount: 200,
|
||||||
region_id: "reg_123",
|
region_id: "reg_123",
|
||||||
})
|
})
|
||||||
@@ -40,11 +32,11 @@ moduleIntegrationTestRunner({
|
|||||||
paymentCollection.id,
|
paymentCollection.id,
|
||||||
{
|
{
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
providerContext: {
|
amount: 200,
|
||||||
amount: 200,
|
currency_code: "usd",
|
||||||
currency_code: "USD",
|
data: {},
|
||||||
payment_session_data: {},
|
context: {
|
||||||
context: {},
|
extra: {},
|
||||||
customer: {},
|
customer: {},
|
||||||
billing_address: {},
|
billing_address: {},
|
||||||
email: "test@test.test.com",
|
email: "test@test.test.com",
|
||||||
@@ -72,7 +64,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect(paymentCollection).toEqual(
|
expect(paymentCollection).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
currency_code: "USD",
|
currency_code: "usd",
|
||||||
amount: 200,
|
amount: 200,
|
||||||
// TODO
|
// TODO
|
||||||
// authorized_amount: 200,
|
// authorized_amount: 200,
|
||||||
@@ -83,7 +75,7 @@ moduleIntegrationTestRunner({
|
|||||||
payment_sessions: [
|
payment_sessions: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
currency_code: "USD",
|
currency_code: "usd",
|
||||||
amount: 200,
|
amount: 200,
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
status: "authorized",
|
status: "authorized",
|
||||||
@@ -94,7 +86,7 @@ moduleIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
amount: 200,
|
amount: 200,
|
||||||
currency_code: "USD",
|
currency_code: "usd",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
captures: [
|
captures: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -336,11 +328,11 @@ moduleIntegrationTestRunner({
|
|||||||
it("should create a payment session successfully", async () => {
|
it("should create a payment session successfully", async () => {
|
||||||
await service.createPaymentSession("pay-col-id-1", {
|
await service.createPaymentSession("pay-col-id-1", {
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
providerContext: {
|
amount: 200,
|
||||||
amount: 200,
|
currency_code: "usd",
|
||||||
currency_code: "usd",
|
data: {},
|
||||||
payment_session_data: {},
|
context: {
|
||||||
context: {},
|
extra: {},
|
||||||
customer: {},
|
customer: {},
|
||||||
billing_address: {},
|
billing_address: {},
|
||||||
email: "test@test.test.com",
|
email: "test@test.test.com",
|
||||||
@@ -377,11 +369,11 @@ moduleIntegrationTestRunner({
|
|||||||
it("should update a payment session successfully", async () => {
|
it("should update a payment session successfully", async () => {
|
||||||
let session = await service.createPaymentSession("pay-col-id-1", {
|
let session = await service.createPaymentSession("pay-col-id-1", {
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
providerContext: {
|
amount: 200,
|
||||||
amount: 200,
|
currency_code: "usd",
|
||||||
currency_code: "usd",
|
data: {},
|
||||||
payment_session_data: {},
|
context: {
|
||||||
context: {},
|
extra: {},
|
||||||
customer: {},
|
customer: {},
|
||||||
billing_address: {},
|
billing_address: {},
|
||||||
email: "test@test.test.com",
|
email: "test@test.test.com",
|
||||||
@@ -391,15 +383,15 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
session = await service.updatePaymentSession({
|
session = await service.updatePaymentSession({
|
||||||
id: session.id,
|
id: session.id,
|
||||||
providerContext: {
|
amount: 200,
|
||||||
amount: 200,
|
currency_code: "eur",
|
||||||
currency_code: "eur",
|
data: {},
|
||||||
|
context: {
|
||||||
resource_id: "res_id",
|
resource_id: "res_id",
|
||||||
context: {},
|
extra: {},
|
||||||
customer: {},
|
customer: {},
|
||||||
billing_address: {},
|
billing_address: {},
|
||||||
email: "new@test.tsst",
|
email: "new@test.tsst",
|
||||||
payment_session_data: {},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -424,11 +416,11 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
const session = await service.createPaymentSession(collection.id, {
|
const session = await service.createPaymentSession(collection.id, {
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
providerContext: {
|
amount: 100,
|
||||||
amount: 100,
|
currency_code: "usd",
|
||||||
currency_code: "usd",
|
data: {},
|
||||||
payment_session_data: {},
|
context: {
|
||||||
context: {},
|
extra: {},
|
||||||
resource_id: "test",
|
resource_id: "test",
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
billing_address: {},
|
billing_address: {},
|
||||||
@@ -447,7 +439,6 @@ moduleIntegrationTestRunner({
|
|||||||
amount: 100,
|
amount: 100,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
provider_id: "pp_system_default",
|
provider_id: "pp_system_default",
|
||||||
|
|
||||||
refunds: [],
|
refunds: [],
|
||||||
captures: [],
|
captures: [],
|
||||||
data: {},
|
data: {},
|
||||||
@@ -458,9 +449,7 @@ moduleIntegrationTestRunner({
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
captured_at: null,
|
captured_at: null,
|
||||||
canceled_at: null,
|
canceled_at: null,
|
||||||
payment_collection: expect.objectContaining({
|
payment_collection_id: expect.any(String),
|
||||||
id: expect.any(String),
|
|
||||||
}),
|
|
||||||
payment_session: expect.objectContaining({
|
payment_session: expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
updated_at: expect.any(Date),
|
updated_at: expect.any(Date),
|
||||||
@@ -472,6 +461,10 @@ moduleIntegrationTestRunner({
|
|||||||
data: {},
|
data: {},
|
||||||
status: "authorized",
|
status: "authorized",
|
||||||
authorized_at: expect.any(Date),
|
authorized_at: expect.any(Date),
|
||||||
|
payment_collection: expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
}),
|
||||||
|
payment_collection_id: expect.any(String),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"@medusajs/modules-sdk": "^1.12.5",
|
"@medusajs/modules-sdk": "^1.12.5",
|
||||||
"@medusajs/types": "^1.11.9",
|
"@medusajs/types": "^1.11.9",
|
||||||
"@medusajs/utils": "^1.11.2",
|
"@medusajs/utils": "^1.11.2",
|
||||||
|
"@medusajs/workflows-sdk": "workspace:^",
|
||||||
"@mikro-orm/core": "5.9.7",
|
"@mikro-orm/core": "5.9.7",
|
||||||
"@mikro-orm/migrations": "5.9.7",
|
"@mikro-orm/migrations": "5.9.7",
|
||||||
"@mikro-orm/postgresql": "5.9.7",
|
"@mikro-orm/postgresql": "5.9.7",
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export class Migration20240225134525 extends Migration {
|
|||||||
ALTER TABLE IF EXISTS "payment_provider" ADD COLUMN IF NOT EXISTS "is_enabled" BOOLEAN NOT NULL DEFAULT TRUE;
|
ALTER TABLE IF EXISTS "payment_provider" ADD COLUMN IF NOT EXISTS "is_enabled" BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
|
|
||||||
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_collection_id" TEXT NOT NULL;
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_collection_id" TEXT NOT NULL;
|
||||||
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "currency_code" TEXT NOT NULL;
|
||||||
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "authorized_at" TEXT NULL;
|
||||||
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_authorized_at" TIMESTAMPTZ NULL;
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "payment_authorized_at" TIMESTAMPTZ NULL;
|
||||||
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||||
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||||
@@ -31,6 +33,7 @@ export class Migration20240225134525 extends Migration {
|
|||||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "provider_id" TEXT NOT NULL;
|
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "provider_id" TEXT NOT NULL;
|
||||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||||
|
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "payment_session_id" TEXT NOT NULL;
|
||||||
|
|
||||||
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||||
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||||
@@ -176,7 +179,7 @@ export class Migration20240225134525 extends Migration {
|
|||||||
"captured_at" TIMESTAMPTZ NULL,
|
"captured_at" TIMESTAMPTZ NULL,
|
||||||
"canceled_at" TIMESTAMPTZ NULL,
|
"canceled_at" TIMESTAMPTZ NULL,
|
||||||
"payment_collection_id" TEXT NOT NULL,
|
"payment_collection_id" TEXT NOT NULL,
|
||||||
"session_id" TEXT NOT NULL,
|
"payment_session_id" TEXT NOT NULL,
|
||||||
"metadata" JSONB NULL,
|
"metadata" JSONB NULL,
|
||||||
CONSTRAINT "payment_pkey" PRIMARY KEY ("id")
|
CONSTRAINT "payment_pkey" PRIMARY KEY ("id")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,12 +84,12 @@ export default class PaymentCollection {
|
|||||||
payment_providers = new Collection<PaymentProvider>(this)
|
payment_providers = new Collection<PaymentProvider>(this)
|
||||||
|
|
||||||
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
|
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
|
||||||
cascade: [Cascade.REMOVE],
|
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
||||||
})
|
})
|
||||||
payment_sessions = new Collection<PaymentSession>(this)
|
payment_sessions = new Collection<PaymentSession>(this)
|
||||||
|
|
||||||
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
|
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
|
||||||
cascade: [Cascade.REMOVE],
|
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
||||||
})
|
})
|
||||||
payments = new Collection<Payment>(this)
|
payments = new Collection<Payment>(this)
|
||||||
|
|
||||||
|
|||||||
@@ -54,19 +54,24 @@ export default class PaymentSession {
|
|||||||
})
|
})
|
||||||
authorized_at: Date | null = null
|
authorized_at: Date | null = null
|
||||||
|
|
||||||
|
@ManyToOne(() => PaymentCollection, {
|
||||||
|
persist: false,
|
||||||
|
})
|
||||||
|
payment_collection: PaymentCollection
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
entity: () => PaymentCollection,
|
entity: () => PaymentCollection,
|
||||||
|
columnType: "text",
|
||||||
index: "IDX_payment_session_payment_collection_id",
|
index: "IDX_payment_session_payment_collection_id",
|
||||||
fieldName: "payment_collection_id",
|
fieldName: "payment_collection_id",
|
||||||
onDelete: "cascade",
|
mapToPk: true,
|
||||||
})
|
})
|
||||||
payment_collection!: PaymentCollection
|
payment_collection_id: string
|
||||||
|
|
||||||
@OneToOne({
|
@OneToOne({
|
||||||
entity: () => Payment,
|
entity: () => Payment,
|
||||||
mappedBy: (payment) => payment.payment_session,
|
|
||||||
cascade: ["soft-remove"] as any,
|
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
mappedBy: "payment_session",
|
||||||
})
|
})
|
||||||
payment?: Payment | null
|
payment?: Payment | null
|
||||||
|
|
||||||
|
|||||||
@@ -109,18 +109,26 @@ export default class Payment {
|
|||||||
captures = new Collection<Capture>(this)
|
captures = new Collection<Capture>(this)
|
||||||
|
|
||||||
@ManyToOne({
|
@ManyToOne({
|
||||||
|
entity: () => PaymentCollection,
|
||||||
|
persist: false,
|
||||||
|
})
|
||||||
|
payment_collection: PaymentCollection
|
||||||
|
|
||||||
|
@ManyToOne({
|
||||||
|
entity: () => PaymentCollection,
|
||||||
|
columnType: "text",
|
||||||
index: "IDX_payment_payment_collection_id",
|
index: "IDX_payment_payment_collection_id",
|
||||||
fieldName: "payment_collection_id",
|
fieldName: "payment_collection_id",
|
||||||
onDelete: "cascade",
|
mapToPk: true,
|
||||||
})
|
})
|
||||||
payment_collection!: PaymentCollection
|
payment_collection_id: string
|
||||||
|
|
||||||
@OneToOne({
|
@OneToOne({
|
||||||
owner: true,
|
owner: true,
|
||||||
fieldName: "session_id",
|
fieldName: "payment_session_id",
|
||||||
index: "IDX_payment_payment_session_id",
|
index: "IDX_payment_payment_session_id",
|
||||||
})
|
})
|
||||||
payment_session!: PaymentSession
|
payment_session: PaymentSession
|
||||||
|
|
||||||
/** COMPUTED PROPERTIES START **/
|
/** COMPUTED PROPERTIES START **/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
PaymentProviderContext,
|
CreatePaymentProviderSession,
|
||||||
PaymentProviderError,
|
PaymentProviderError,
|
||||||
PaymentProviderSessionResponse,
|
PaymentProviderSessionResponse,
|
||||||
PaymentSessionStatus,
|
PaymentSessionStatus,
|
||||||
@@ -21,7 +21,7 @@ export class SystemProviderService extends AbstractPaymentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initiatePayment(
|
async initiatePayment(
|
||||||
context: PaymentProviderContext
|
context: CreatePaymentProviderSession
|
||||||
): Promise<PaymentProviderSessionResponse> {
|
): Promise<PaymentProviderSessionResponse> {
|
||||||
return { data: {} }
|
return { data: {} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
UpdatePaymentSessionDTO,
|
UpdatePaymentSessionDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
@@ -35,7 +36,6 @@ import {
|
|||||||
PaymentSession,
|
PaymentSession,
|
||||||
Refund,
|
Refund,
|
||||||
} from "@models"
|
} from "@models"
|
||||||
|
|
||||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||||
import PaymentProviderService from "./payment-provider"
|
import PaymentProviderService from "./payment-provider"
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ type InjectedDependencies = {
|
|||||||
paymentProviderService: PaymentProviderService
|
paymentProviderService: PaymentProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateMethodForModels = [PaymentCollection, Payment]
|
const generateMethodForModels = [PaymentCollection, Payment, PaymentSession]
|
||||||
|
|
||||||
export default class PaymentModuleService<
|
export default class PaymentModuleService<
|
||||||
TPaymentCollection extends PaymentCollection = PaymentCollection,
|
TPaymentCollection extends PaymentCollection = PaymentCollection,
|
||||||
@@ -201,44 +201,67 @@ export default class PaymentModuleService<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager("baseRepository_")
|
@InjectManager("baseRepository_")
|
||||||
async createPaymentSession(
|
async createPaymentSession(
|
||||||
|
paymentCollectionId: string,
|
||||||
|
input: CreatePaymentSessionDTO,
|
||||||
|
@MedusaContext() sharedContext?: Context
|
||||||
|
): Promise<PaymentSessionDTO> {
|
||||||
|
let paymentSession: PaymentSession
|
||||||
|
|
||||||
|
try {
|
||||||
|
const providerSessionSession =
|
||||||
|
await this.paymentProviderService_.createSession(input.provider_id, {
|
||||||
|
context: input.context ?? {},
|
||||||
|
amount: input.amount,
|
||||||
|
currency_code: input.currency_code,
|
||||||
|
})
|
||||||
|
|
||||||
|
input.data = {
|
||||||
|
...input.data,
|
||||||
|
...providerSessionSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentSession = await this.createPaymentSession_(
|
||||||
|
paymentCollectionId,
|
||||||
|
input,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
// In case the session is created at the provider, but fails to be created in Medusa,
|
||||||
|
// we catch the error and delete the session at the provider and rethrow.
|
||||||
|
await this.paymentProviderService_.deleteSession({
|
||||||
|
provider_id: input.provider_id,
|
||||||
|
data: input.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.baseRepository_.serialize(paymentSession, {
|
||||||
|
populate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
async createPaymentSession_(
|
||||||
paymentCollectionId: string,
|
paymentCollectionId: string,
|
||||||
data: CreatePaymentSessionDTO,
|
data: CreatePaymentSessionDTO,
|
||||||
@MedusaContext() sharedContext?: Context
|
@MedusaContext() sharedContext?: Context
|
||||||
): Promise<PaymentSessionDTO> {
|
): Promise<PaymentSession> {
|
||||||
const created = await this.paymentSessionService_.create(
|
const paymentSession = await this.paymentSessionService_.create(
|
||||||
{
|
{
|
||||||
|
payment_collection_id: paymentCollectionId,
|
||||||
provider_id: data.provider_id,
|
provider_id: data.provider_id,
|
||||||
amount: data.providerContext.amount,
|
amount: data.amount,
|
||||||
currency_code: data.providerContext.currency_code,
|
currency_code: data.currency_code,
|
||||||
payment_collection: paymentCollectionId,
|
context: data.context,
|
||||||
|
data: data.data,
|
||||||
},
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
return paymentSession
|
||||||
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_")
|
@InjectTransactionManager("baseRepository_")
|
||||||
@@ -252,17 +275,12 @@ export default class PaymentModuleService<
|
|||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionData = await this.paymentProviderService_.updateSession(
|
|
||||||
session.provider_id,
|
|
||||||
data.providerContext
|
|
||||||
)
|
|
||||||
|
|
||||||
const updated = await this.paymentSessionService_.update(
|
const updated = await this.paymentSessionService_.update(
|
||||||
{
|
{
|
||||||
id: session.id,
|
id: session.id,
|
||||||
amount: data.providerContext.amount,
|
amount: data.amount,
|
||||||
currency_code: data.providerContext.currency_code,
|
currency_code: data.currency_code,
|
||||||
data: sessionData,
|
data: data.data,
|
||||||
},
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
@@ -298,8 +316,14 @@ export default class PaymentModuleService<
|
|||||||
const session = await this.paymentSessionService_.retrieve(
|
const session = await this.paymentSessionService_.retrieve(
|
||||||
id,
|
id,
|
||||||
{
|
{
|
||||||
select: ["id", "data", "provider_id", "amount", "currency_code"],
|
select: [
|
||||||
relations: ["payment_collection"],
|
"id",
|
||||||
|
"data",
|
||||||
|
"provider_id",
|
||||||
|
"amount",
|
||||||
|
"currency_code",
|
||||||
|
"payment_collection_id",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
@@ -348,7 +372,7 @@ export default class PaymentModuleService<
|
|||||||
amount: session.amount,
|
amount: session.amount,
|
||||||
currency_code: session.currency_code,
|
currency_code: session.currency_code,
|
||||||
payment_session: session.id,
|
payment_session: session.id,
|
||||||
payment_collection: session.payment_collection!.id,
|
payment_collection_id: session.payment_collection_id,
|
||||||
provider_id: session.provider_id,
|
provider_id: session.provider_id,
|
||||||
// customer_id: context.customer.id,
|
// customer_id: context.customer.id,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import { EOL } from "os"
|
|
||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
|
||||||
import {
|
import {
|
||||||
Context,
|
Context,
|
||||||
CreatePaymentProviderDTO,
|
CreatePaymentProviderDTO,
|
||||||
|
CreatePaymentProviderSession,
|
||||||
DAL,
|
DAL,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
IPaymentProvider,
|
IPaymentProvider,
|
||||||
PaymentProviderAuthorizeResponse,
|
PaymentProviderAuthorizeResponse,
|
||||||
PaymentProviderContext,
|
|
||||||
PaymentProviderDataInput,
|
PaymentProviderDataInput,
|
||||||
PaymentProviderError,
|
PaymentProviderError,
|
||||||
PaymentProviderSessionResponse,
|
PaymentProviderSessionResponse,
|
||||||
PaymentSessionStatus,
|
PaymentSessionStatus,
|
||||||
ProviderWebhookPayload,
|
ProviderWebhookPayload,
|
||||||
|
UpdatePaymentProviderSession,
|
||||||
WebhookActionResult,
|
WebhookActionResult,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
@@ -21,8 +20,9 @@ import {
|
|||||||
isPaymentProviderError,
|
isPaymentProviderError,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { PaymentProvider } from "@models"
|
import { PaymentProvider } from "@models"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { EOL } from "os"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
paymentProviderRepository: DAL.RepositoryService
|
paymentProviderRepository: DAL.RepositoryService
|
||||||
@@ -70,20 +70,10 @@ export default class PaymentProviderService {
|
|||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
providerId: string,
|
providerId: string,
|
||||||
sessionInput: PaymentProviderContext
|
sessionInput: CreatePaymentProviderSession
|
||||||
): Promise<PaymentProviderSessionResponse["data"]> {
|
): Promise<PaymentProviderSessionResponse["data"]> {
|
||||||
const provider = this.retrieveProvider(providerId)
|
const provider = this.retrieveProvider(providerId)
|
||||||
|
|
||||||
if (
|
|
||||||
!isDefined(sessionInput.currency_code) ||
|
|
||||||
!isDefined(sessionInput.amount)
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
|
||||||
"`currency_code` and `amount` are required to create payment session."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const paymentResponse = await provider.initiatePayment(sessionInput)
|
const paymentResponse = await provider.initiatePayment(sessionInput)
|
||||||
|
|
||||||
if (isPaymentProviderError(paymentResponse)) {
|
if (isPaymentProviderError(paymentResponse)) {
|
||||||
@@ -95,7 +85,7 @@ export default class PaymentProviderService {
|
|||||||
|
|
||||||
async updateSession(
|
async updateSession(
|
||||||
providerId: string,
|
providerId: string,
|
||||||
sessionInput: PaymentProviderContext
|
sessionInput: UpdatePaymentProviderSession
|
||||||
): Promise<Record<string, unknown> | undefined> {
|
): Promise<Record<string, unknown> | undefined> {
|
||||||
const provider = this.retrieveProvider(providerId)
|
const provider = this.retrieveProvider(providerId)
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,19 @@ export interface FilterablePaymentCollectionProps
|
|||||||
updated_at?: OperatorMap<string>
|
updated_at?: OperatorMap<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilterablePaymentSessionProps
|
||||||
|
extends BaseFilterable<PaymentSessionDTO> {
|
||||||
|
id?: string | string[]
|
||||||
|
currency_code?: string | string[]
|
||||||
|
amount?: number | OperatorMap<number>
|
||||||
|
provider_id?: string | string[]
|
||||||
|
payment_collection_id?: string | string[]
|
||||||
|
region_id?: string | string[] | OperatorMap<string>
|
||||||
|
created_at?: OperatorMap<string>
|
||||||
|
updated_at?: OperatorMap<string>
|
||||||
|
deleted_at?: OperatorMap<string>
|
||||||
|
}
|
||||||
|
|
||||||
/* ********** PAYMENT ********** */
|
/* ********** PAYMENT ********** */
|
||||||
export interface PaymentDTO {
|
export interface PaymentDTO {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -186,9 +186,21 @@ export interface CreatePaymentSessionDTO {
|
|||||||
*/
|
*/
|
||||||
provider_id: string
|
provider_id: string
|
||||||
/**
|
/**
|
||||||
* The provider's context.
|
* The selected currency code.
|
||||||
*/
|
*/
|
||||||
providerContext: Omit<PaymentProviderContext, "resource_id">
|
currency_code: string
|
||||||
|
/**
|
||||||
|
* The payment's amount.
|
||||||
|
*/
|
||||||
|
amount: number
|
||||||
|
/**
|
||||||
|
* The value of the payment session's `data` field.
|
||||||
|
*/
|
||||||
|
data: Record<string, unknown>
|
||||||
|
/**
|
||||||
|
* The payment session's context.
|
||||||
|
*/
|
||||||
|
context?: PaymentProviderContext
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,10 +211,22 @@ export interface UpdatePaymentSessionDTO {
|
|||||||
* The payment session's ID.
|
* The payment session's ID.
|
||||||
*/
|
*/
|
||||||
id: string
|
id: string
|
||||||
|
/**
|
||||||
|
* The value of the payment session's `data` field.
|
||||||
|
*/
|
||||||
|
data: Record<string, unknown>
|
||||||
|
/**
|
||||||
|
* The selected currency code.
|
||||||
|
*/
|
||||||
|
currency_code: string
|
||||||
|
/**
|
||||||
|
* The payment's amount.
|
||||||
|
*/
|
||||||
|
amount: number
|
||||||
/**
|
/**
|
||||||
* The payment session's context.
|
* The payment session's context.
|
||||||
*/
|
*/
|
||||||
providerContext: PaymentProviderContext
|
context?: PaymentProviderContext
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PaymentSessionStatus } from "./common"
|
|
||||||
import { CustomerDTO } from "../customer"
|
|
||||||
import { AddressDTO } from "../address"
|
import { AddressDTO } from "../address"
|
||||||
|
import { CustomerDTO } from "../customer"
|
||||||
|
import { PaymentSessionStatus } from "./common"
|
||||||
import { ProviderWebhookPayload } from "./mutations"
|
import { ProviderWebhookPayload } from "./mutations"
|
||||||
|
|
||||||
export type PaymentAddressDTO = Partial<AddressDTO>
|
export type PaymentAddressDTO = Partial<AddressDTO>
|
||||||
@@ -43,31 +43,31 @@ export type PaymentProviderContext = {
|
|||||||
* The customer's email.
|
* The customer's email.
|
||||||
*/
|
*/
|
||||||
email?: string
|
email?: string
|
||||||
/**
|
|
||||||
* The selected currency code.
|
|
||||||
*/
|
|
||||||
currency_code: string
|
|
||||||
/**
|
|
||||||
* The payment's amount.
|
|
||||||
*/
|
|
||||||
amount: number
|
|
||||||
/**
|
/**
|
||||||
* The ID of the resource the payment is associated with i.e. the ID of the PaymentSession in Medusa
|
* The ID of the resource the payment is associated with i.e. the ID of the PaymentSession in Medusa
|
||||||
*/
|
*/
|
||||||
resource_id: string
|
resource_id?: string
|
||||||
/**
|
/**
|
||||||
* The customer associated with this payment.
|
* The customer associated with this payment.
|
||||||
*/
|
*/
|
||||||
customer?: PaymentCustomerDTO
|
customer?: PaymentCustomerDTO
|
||||||
/**
|
/**
|
||||||
* The context.
|
* The extra fields specific to the provider session.
|
||||||
*/
|
*/
|
||||||
context: { payment_description?: string } & Record<string, unknown>
|
extra?: 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.
|
export type CreatePaymentProviderSession = {
|
||||||
*/
|
context: PaymentProviderContext
|
||||||
payment_session_data: Record<string, unknown>
|
amount: number
|
||||||
|
currency_code: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdatePaymentProviderSession = {
|
||||||
|
context: PaymentProviderContext
|
||||||
|
data: Record<string, unknown>
|
||||||
|
amount: number
|
||||||
|
currency_code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,21 +142,21 @@ export interface IPaymentProvider {
|
|||||||
/**
|
/**
|
||||||
* Make calls to the third-party provider to initialize the payment. For example, in Stripe this method is used to create a Payment Intent for the customer.
|
* Make calls to the third-party provider to initialize the payment. For example, in Stripe this method is used to create a Payment Intent for the customer.
|
||||||
*
|
*
|
||||||
* @param {PaymentProviderContext} context - The context of the payment.
|
* @param {CreatePaymentProviderSession} context
|
||||||
* @returns {Promise<PaymentProviderError | PaymentProviderSessionResponse>} Either the payment's data or an error object.
|
* @returns {Promise<PaymentProviderError | PaymentProviderSessionResponse>} Either the payment's data or an error object.
|
||||||
*/
|
*/
|
||||||
initiatePayment(
|
initiatePayment(
|
||||||
context: PaymentProviderContext
|
data: CreatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is used to update the payment session.
|
* This method is used to update the payment session.
|
||||||
*
|
*
|
||||||
* @param {PaymentProviderContext} context - The context of the payment.
|
* @param {UpdatePaymentProviderSession} context
|
||||||
* @returns {Promise<PaymentProviderError | PaymentProviderSessionResponse | void>} Either the payment's data or an error object.
|
* @returns {Promise<PaymentProviderError | PaymentProviderSessionResponse | void>} Either the payment's data or an error object.
|
||||||
*/
|
*/
|
||||||
updatePayment(
|
updatePayment(
|
||||||
context: PaymentProviderContext
|
context: UpdatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Context } from "../shared-context"
|
|||||||
import {
|
import {
|
||||||
FilterablePaymentCollectionProps,
|
FilterablePaymentCollectionProps,
|
||||||
FilterablePaymentProps,
|
FilterablePaymentProps,
|
||||||
|
FilterablePaymentSessionProps,
|
||||||
PaymentCollectionDTO,
|
PaymentCollectionDTO,
|
||||||
PaymentDTO,
|
PaymentDTO,
|
||||||
PaymentSessionDTO,
|
PaymentSessionDTO,
|
||||||
@@ -387,6 +388,12 @@ export interface IPaymentModuleService extends IModuleService {
|
|||||||
*/
|
*/
|
||||||
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
|
cancelPayment(paymentId: string, sharedContext?: Context): Promise<PaymentDTO>
|
||||||
|
|
||||||
|
listPaymentSessions(
|
||||||
|
filters?: FilterablePaymentSessionProps,
|
||||||
|
config?: FindConfig<PaymentSessionDTO>,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<PaymentSessionDTO[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method creates providers on load.
|
* This method creates providers on load.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
CreatePaymentProviderSession,
|
||||||
IPaymentProvider,
|
IPaymentProvider,
|
||||||
MedusaContainer,
|
MedusaContainer,
|
||||||
PaymentProviderContext,
|
|
||||||
PaymentProviderError,
|
PaymentProviderError,
|
||||||
PaymentProviderSessionResponse,
|
PaymentProviderSessionResponse,
|
||||||
PaymentSessionStatus,
|
PaymentSessionStatus,
|
||||||
ProviderWebhookPayload,
|
ProviderWebhookPayload,
|
||||||
WebhookActionResult,
|
UpdatePaymentProviderSession,
|
||||||
|
WebhookActionResult
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
|
export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
|
||||||
@@ -107,7 +108,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
|
|||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
|
||||||
|
|
||||||
abstract initiatePayment(
|
abstract initiatePayment(
|
||||||
context: PaymentProviderContext
|
context: CreatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
||||||
|
|
||||||
abstract deletePayment(
|
abstract deletePayment(
|
||||||
@@ -128,7 +129,7 @@ export abstract class AbstractPaymentProvider<TConfig = Record<string, unknown>>
|
|||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]>
|
||||||
|
|
||||||
abstract updatePayment(
|
abstract updatePayment(
|
||||||
context: PaymentProviderContext
|
context: UpdatePaymentProviderSession
|
||||||
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
): Promise<PaymentProviderError | PaymentProviderSessionResponse>
|
||||||
|
|
||||||
abstract getWebhookActionAndData(
|
abstract getWebhookActionAndData(
|
||||||
|
|||||||
@@ -8642,6 +8642,7 @@ __metadata:
|
|||||||
"@medusajs/modules-sdk": ^1.12.5
|
"@medusajs/modules-sdk": ^1.12.5
|
||||||
"@medusajs/types": ^1.11.9
|
"@medusajs/types": ^1.11.9
|
||||||
"@medusajs/utils": ^1.11.2
|
"@medusajs/utils": ^1.11.2
|
||||||
|
"@medusajs/workflows-sdk": "workspace:^"
|
||||||
"@mikro-orm/cli": 5.9.7
|
"@mikro-orm/cli": 5.9.7
|
||||||
"@mikro-orm/core": 5.9.7
|
"@mikro-orm/core": 5.9.7
|
||||||
"@mikro-orm/migrations": 5.9.7
|
"@mikro-orm/migrations": 5.9.7
|
||||||
@@ -9126,7 +9127,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@medusajs/workflows-sdk@^0.1.2, @medusajs/workflows-sdk@^0.1.3, @medusajs/workflows-sdk@workspace:packages/workflows-sdk":
|
"@medusajs/workflows-sdk@^0.1.2, @medusajs/workflows-sdk@^0.1.3, @medusajs/workflows-sdk@workspace:^, @medusajs/workflows-sdk@workspace:packages/workflows-sdk":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/workflows-sdk@workspace:packages/workflows-sdk"
|
resolution: "@medusajs/workflows-sdk@workspace:packages/workflows-sdk"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in new issue
Block a user