Skip to content

Commit 971c03c

Browse files
committed
fix(core-flows,workflows-sdk): compensate account holders only when its created
1 parent d517dbd commit 971c03c

5 files changed

Lines changed: 166 additions & 21 deletions

File tree

.changeset/fair-glasses-push.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@medusajs/workflows-sdk": patch
3+
"@medusajs/core-flows": patch
4+
---
5+
6+
fix(core-flows,workflows-sdk): compensate account holders only when its created

integration-tests/modules/__tests__/payment/payment-session.workflows.spec.ts

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import {
22
createPaymentSessionsWorkflow,
33
createPaymentSessionsWorkflowId,
44
} from "@medusajs/core-flows"
5-
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
6-
import { Modules } from "@medusajs/utils"
75
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
6+
import {
7+
ICustomerModuleService,
8+
IPaymentModuleService,
9+
IRegionModuleService,
10+
} from "@medusajs/types"
11+
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
812

913
jest.setTimeout(50000)
1014

@@ -17,18 +21,21 @@ medusaIntegrationTestRunner({
1721
let appContainer
1822
let paymentModule: IPaymentModuleService
1923
let regionModule: IRegionModuleService
20-
let remoteLink
24+
let customerModule: ICustomerModuleService
25+
let query
2126

2227
beforeAll(async () => {
2328
appContainer = getContainer()
2429
paymentModule = appContainer.resolve(Modules.PAYMENT)
2530
regionModule = appContainer.resolve(Modules.REGION)
26-
remoteLink = appContainer.resolve("remoteLink")
31+
customerModule = appContainer.resolve(Modules.CUSTOMER)
32+
query = appContainer.resolve(ContainerRegistrationKeys.QUERY)
2733
})
2834

2935
describe("createPaymentSessionWorkflow", () => {
3036
let region
3137
let paymentCollection
38+
let customer
3239

3340
beforeEach(async () => {
3441
region = await regionModule.createRegions({
@@ -40,6 +47,12 @@ medusaIntegrationTestRunner({
4047
currency_code: "usd",
4148
amount: 1000,
4249
})
50+
51+
customer = await customerModule.createCustomers({
52+
email: "test@test.com",
53+
first_name: "Test",
54+
last_name: "Test",
55+
})
4356
})
4457

4558
it("should create payment sessions", async () => {
@@ -75,6 +88,47 @@ medusaIntegrationTestRunner({
7588
)
7689
})
7790

91+
it("should create payment sessions with customer", async () => {
92+
await createPaymentSessionsWorkflow(appContainer).run({
93+
input: {
94+
payment_collection_id: paymentCollection.id,
95+
provider_id: "pp_system_default",
96+
customer_id: customer.id,
97+
},
98+
})
99+
100+
const {
101+
data: [updatedPaymentCollection],
102+
} = await query.graph({
103+
entity: "payment_collection",
104+
filters: {
105+
id: paymentCollection.id,
106+
},
107+
fields: ["id", "currency_code", "amount", "payment_sessions.*"],
108+
})
109+
110+
expect(updatedPaymentCollection.payment_sessions).toHaveLength(1)
111+
expect(updatedPaymentCollection).toEqual(
112+
expect.objectContaining({
113+
id: paymentCollection.id,
114+
currency_code: "usd",
115+
amount: 1000,
116+
payment_sessions: expect.arrayContaining([
117+
expect.objectContaining({
118+
context: expect.objectContaining({
119+
customer: expect.objectContaining({
120+
id: customer.id,
121+
}),
122+
account_holder: expect.objectContaining({
123+
email: customer.email,
124+
}),
125+
}),
126+
}),
127+
]),
128+
})
129+
)
130+
})
131+
78132
it("should delete existing sessions when create payment sessions", async () => {
79133
await createPaymentSessionsWorkflow(appContainer).run({
80134
input: {
@@ -164,6 +218,89 @@ medusaIntegrationTestRunner({
164218

165219
expect(sessions).toHaveLength(0)
166220
})
221+
222+
it.only("should not delete account holder if it exists before creating payment sessions", async () => {
223+
await createPaymentSessionsWorkflow(appContainer).run({
224+
input: {
225+
payment_collection_id: paymentCollection.id,
226+
provider_id: "pp_system_default",
227+
customer_id: customer.id,
228+
},
229+
})
230+
231+
const {
232+
data: [updatedCustomer1],
233+
} = await query.graph({
234+
entity: "customer",
235+
filters: {
236+
id: customer.id,
237+
},
238+
fields: ["id", "account_holders.*"],
239+
})
240+
241+
expect(updatedCustomer1.account_holders).toEqual(
242+
expect.arrayContaining([
243+
expect.objectContaining({
244+
email: customer.email,
245+
}),
246+
])
247+
)
248+
249+
const newPaymentCollection =
250+
await paymentModule.createPaymentCollections({
251+
currency_code: "usd",
252+
amount: 2000,
253+
})
254+
255+
const workflow = createPaymentSessionsWorkflow(appContainer)
256+
257+
workflow.appendAction("throw", createPaymentSessionsWorkflowId, {
258+
invoke: async function failStep() {
259+
throw new Error(
260+
`Failed to do something after creating payment sessions`
261+
)
262+
},
263+
})
264+
265+
const { errors } = await workflow.run({
266+
input: {
267+
payment_collection_id: newPaymentCollection.id,
268+
provider_id: "pp_system_default",
269+
customer_id: customer.id,
270+
context: {},
271+
data: {},
272+
},
273+
throwOnError: false,
274+
})
275+
276+
expect(errors).toEqual([
277+
{
278+
action: "throw",
279+
handlerType: "invoke",
280+
error: expect.objectContaining({
281+
message: `Failed to do something after creating payment sessions`,
282+
}),
283+
},
284+
])
285+
286+
const {
287+
data: [updatedCustomer2],
288+
} = await query.graph({
289+
entity: "customer",
290+
filters: {
291+
id: customer.id,
292+
},
293+
fields: ["id", "account_holders.*"],
294+
})
295+
296+
expect(updatedCustomer2.account_holders).toEqual(
297+
expect.arrayContaining([
298+
expect.objectContaining({
299+
email: customer.email,
300+
}),
301+
])
302+
)
303+
})
167304
})
168305
})
169306
})

packages/core/core-flows/src/payment-collection/steps/create-payment-account-holder.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import {
2-
IPaymentModuleService,
32
CreateAccountHolderDTO,
3+
IPaymentModuleService,
44
} from "@medusajs/framework/types"
5-
import { Modules } from "@medusajs/framework/utils"
6-
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
5+
import { isPresent, Modules } from "@medusajs/framework/utils"
6+
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
77

88
export const createPaymentAccountHolderStepId = "create-payment-account-holder"
99
/**
1010
* This step creates the account holder in the payment provider.
11-
*
11+
*
1212
* @example
1313
* const accountHolder = createPaymentAccountHolderStep({
1414
* provider_id: "pp_stripe_stripe",
@@ -27,7 +27,13 @@ export const createPaymentAccountHolderStep = createStep(
2727

2828
const accountHolder = await service.createAccountHolder(data)
2929

30-
return new StepResponse(accountHolder, accountHolder)
30+
// createAccountHolder is an idempotent operation.
31+
// We pass the account holder to the compensation step if it was actually created to avoid deleting the existing account holder.
32+
const createdAccountHolder = isPresent(data.context.account_holder)
33+
? null
34+
: accountHolder
35+
36+
return new StepResponse(accountHolder, createdAccountHolder)
3137
},
3238
async (createdAccountHolder, { container }) => {
3339
if (!createdAccountHolder) {

packages/core/core-flows/src/payment-collection/workflows/create-payment-session.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const createPaymentSessionsWorkflow = createWorkflow(
8282
list: false,
8383
}).config({ name: "get-payment-collection" })
8484

85-
const { paymentCustomer, accountHolder } = when(
85+
const { paymentCustomer, accountHolder, existingAccountHolder } = when(
8686
"customer-id-exists",
8787
{ input },
8888
(data) => {
@@ -138,20 +138,14 @@ export const createPaymentSessionsWorkflow = createWorkflow(
138138

139139
const accountHolder = createPaymentAccountHolderStep(accountHolderInput)
140140

141-
return { paymentCustomer, accountHolder }
141+
return { paymentCustomer, accountHolder, existingAccountHolder }
142142
})
143143

144144
when(
145145
"account-holder-created",
146-
{ paymentCustomer, accountHolder, input },
147-
(data) => {
148-
return (
149-
!isPresent(
150-
data.paymentCustomer?.account_holders.find(
151-
(ac) => ac.provider_id === data.input.provider_id
152-
)
153-
) && isPresent(data.accountHolder)
154-
)
146+
{ paymentCustomer, accountHolder, input, existingAccountHolder },
147+
({ existingAccountHolder, accountHolder }) => {
148+
return !isPresent(existingAccountHolder) && isPresent(accountHolder)
155149
}
156150
).then(() => {
157151
createRemoteLinkStep([

packages/core/workflows-sdk/src/utils/composer/helpers/step-response.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export class StepResponse<TOutput, TCompensateInput = TOutput> {
3838
if (isDefined(output)) {
3939
this.#output = output
4040
}
41-
this.#compensateInput = (compensateInput ?? output) as TCompensateInput
41+
this.#compensateInput = (
42+
isDefined(compensateInput) ? compensateInput : output
43+
) as TCompensateInput
4244
}
4345

4446
/**

0 commit comments

Comments
 (0)