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:
Oli Juhl authored and GitHub committed 2024-03-05 08:40:47 +00:00
1 parent 908b1dc3a2
commit 84208aafc1
30 files changed
+594 -173

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 -1
View File
@@ -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"
@@ -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)
}
)
@@ -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({
@@ -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)],
},
]
@@ -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 })
}
@@ -0,0 +1,7 @@
import { PaymentProviderContext } from "@medusajs/types"
export class StorePostPaymentCollectionsPaymentSessionReq {
provider_id: string
context?: PaymentProviderContext
data?: Record<string, unknown>
}
+22 -26
View File
@@ -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: {},
@@ -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",
payment_session_data: {}, 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",
payment_session_data: {}, 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",
payment_session_data: {}, 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",
payment_session_data: {}, 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),
}), }),
}) })
) )
+1
View File
@@ -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
+12 -4
View File
@@ -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 **/
+2 -2
View File
@@ -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: {} }
} }
+65 -41
View File
@@ -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)
+13
View File
@@ -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 {
/** /**
+27 -3
View File
@@ -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
} }
/** /**
+22 -22
View File
@@ -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>
/** /**
+7
View File
@@ -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(
+2 -1
View File
@@ -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: