feat: Refresh payment collection + delete session (#6594)
### What Add workflow for refreshing a payment collection. The idea is that on all cart updates, we want two things to happen (in the context of payments) 1. the currently active payment sessions should be destroyed, and 2. the payment collection should be updated with the new cart total. We do this to ensure that we always collect the correct payment amount. From a customer perspective, this would mean that every time something on the cart is updated, the customer would need to enter their payment details anew. To me, this is a good tradeoff to avoid inconsistencies with payment collection. Additionally, I updated the Payment Module interface with `upsert` and `updated` following our established convention. ### Note This PR depends on a fix to the `remoteJoiner` that @carlos-r-l-rodrigues is working on. Update: Fix merged in #6602 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
@@ -6,10 +6,12 @@ import {
|
||||
deleteLineItemsWorkflow,
|
||||
findOrCreateCustomerStepId,
|
||||
linkCartAndPaymentCollectionsStepId,
|
||||
refreshPaymentCollectionForCartWorkflow,
|
||||
updateLineItemInCartWorkflow,
|
||||
updateLineItemsStepId,
|
||||
updatePaymentCollectionStepId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ICartModuleService,
|
||||
ICustomerModuleService,
|
||||
@@ -19,8 +21,8 @@ import {
|
||||
IRegionModuleService,
|
||||
ISalesChannelModuleService,
|
||||
} from "@medusajs/types"
|
||||
import adminSeeder from "../../../../helpers/admin-seeder"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import adminSeeder from "../../../../helpers/admin-seeder"
|
||||
|
||||
jest.setTimeout(200000)
|
||||
|
||||
@@ -664,89 +666,12 @@ medusaIntegrationTestRunner({
|
||||
expect(updatedItem).not.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("createPaymentCollectionForCart", () => {
|
||||
it("should create a payment collection and link it to cart", async () => {
|
||||
const region = await regionModuleService.create({
|
||||
name: "US",
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
items: [
|
||||
{
|
||||
quantity: 1,
|
||||
unit_price: 5000,
|
||||
title: "Test item",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await createPaymentCollectionForCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
region_id: region.id,
|
||||
currency_code: "usd",
|
||||
amount: 5000,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
const result = await remoteQuery(
|
||||
{
|
||||
cart: {
|
||||
fields: ["id"],
|
||||
payment_collection: {
|
||||
fields: ["id", "amount", "currency_code"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cart: {
|
||||
id: cart.id,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
payment_collection: expect.objectContaining({
|
||||
amount: 5000,
|
||||
currency_code: "usd",
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe("compensation", () => {
|
||||
it("should dismiss cart <> payment collection link and delete created payment collection", async () => {
|
||||
const workflow =
|
||||
createPaymentCollectionForCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction(
|
||||
"throw",
|
||||
linkCartAndPaymentCollectionsStepId,
|
||||
{
|
||||
invoke: async function failStep() {
|
||||
throw new Error(
|
||||
`Failed to do something after linking cart and payment collection`
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const region = await regionModuleService.create({
|
||||
name: "US",
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
describe("createPaymentCollectionForCart", () => {
|
||||
it("should create a payment collection and link it to cart", async () => {
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
currency_code: "dkk",
|
||||
region_id: defaultRegion.id,
|
||||
items: [
|
||||
{
|
||||
quantity: 1,
|
||||
@@ -756,27 +681,17 @@ medusaIntegrationTestRunner({
|
||||
],
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
await createPaymentCollectionForCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
region_id: region.id,
|
||||
currency_code: "usd",
|
||||
region_id: defaultRegion.id,
|
||||
currency_code: "dkk",
|
||||
amount: 5000,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(
|
||||
`Failed to do something after linking cart and payment collection`
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
const carts = await remoteQuery(
|
||||
const result = await remoteQuery(
|
||||
{
|
||||
cart: {
|
||||
fields: ["id"],
|
||||
@@ -792,19 +707,274 @@ medusaIntegrationTestRunner({
|
||||
}
|
||||
)
|
||||
|
||||
const payCols = await remoteQuery({
|
||||
payment_collection: {
|
||||
fields: ["id"],
|
||||
expect(result).toEqual([
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
payment_collection: expect.objectContaining({
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
}),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe("compensation", () => {
|
||||
it("should dismiss cart <> payment collection link and delete created payment collection", async () => {
|
||||
const workflow =
|
||||
createPaymentCollectionForCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction(
|
||||
"throw",
|
||||
linkCartAndPaymentCollectionsStepId,
|
||||
{
|
||||
invoke: async function failStep() {
|
||||
throw new Error(
|
||||
`Failed to do something after linking cart and payment collection`
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const region = await regionModuleService.create({
|
||||
name: "US",
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
items: [
|
||||
{
|
||||
quantity: 1,
|
||||
unit_price: 5000,
|
||||
title: "Test item",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
region_id: region.id,
|
||||
currency_code: "usd",
|
||||
amount: 5000,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(
|
||||
`Failed to do something after linking cart and payment collection`
|
||||
),
|
||||
},
|
||||
])
|
||||
|
||||
const carts = await remoteQuery(
|
||||
{
|
||||
cart: {
|
||||
fields: ["id"],
|
||||
payment_collection: {
|
||||
fields: ["id", "amount", "currency_code"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cart: {
|
||||
id: cart.id,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const payCols = await remoteQuery({
|
||||
payment_collection: {
|
||||
fields: ["id"],
|
||||
},
|
||||
})
|
||||
|
||||
expect(carts).toEqual([
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
payment_collection: undefined,
|
||||
}),
|
||||
])
|
||||
expect(payCols.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("refreshPaymentCollectionForCart", () => {
|
||||
it("should refresh a payment collection for a cart", async () => {
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "dkk",
|
||||
region_id: defaultRegion.id,
|
||||
items: [
|
||||
{
|
||||
quantity: 1,
|
||||
unit_price: 5000,
|
||||
title: "Test item",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const paymentCollection =
|
||||
await paymentModule.createPaymentCollections({
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
region_id: defaultRegion.id,
|
||||
})
|
||||
|
||||
const paymentSession = await paymentModule.createPaymentSession(
|
||||
paymentCollection.id,
|
||||
{
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
data: {},
|
||||
provider_id: "pp_system_default",
|
||||
}
|
||||
)
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.CART]: {
|
||||
cart_id: cart.id,
|
||||
},
|
||||
[Modules.PAYMENT]: {
|
||||
payment_collection_id: paymentCollection.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
await refreshPaymentCollectionForCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
const updatedPaymentCollection =
|
||||
await paymentModule.retrievePaymentCollection(paymentCollection.id)
|
||||
|
||||
expect(updatedPaymentCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
id: paymentCollection.id,
|
||||
amount: 4242,
|
||||
})
|
||||
)
|
||||
|
||||
const sessionShouldNotExist = await paymentModule.listPaymentSessions(
|
||||
{ id: paymentSession.id },
|
||||
{ withDeleted: true }
|
||||
)
|
||||
|
||||
expect(sessionShouldNotExist).toHaveLength(0)
|
||||
})
|
||||
|
||||
describe("compensation", () => {
|
||||
it("should revert payment collection amount and create a new payment session", async () => {
|
||||
const region = await regionModuleService.create({
|
||||
name: "US",
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const testCart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
items: [
|
||||
{
|
||||
quantity: 1,
|
||||
unit_price: 5000,
|
||||
title: "Test item",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const paymentCollection =
|
||||
await paymentModule.createPaymentCollections({
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
region_id: defaultRegion.id,
|
||||
})
|
||||
|
||||
const paymentSession = await paymentModule.createPaymentSession(
|
||||
paymentCollection.id,
|
||||
{
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
data: {},
|
||||
provider_id: "pp_system_default",
|
||||
}
|
||||
)
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.CART]: {
|
||||
cart_id: testCart.id,
|
||||
},
|
||||
[Modules.PAYMENT]: {
|
||||
payment_collection_id: paymentCollection.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const workflow =
|
||||
refreshPaymentCollectionForCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction("throw", updatePaymentCollectionStepId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(
|
||||
`Failed to do something after updating payment collections`
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
expect(carts).toEqual([
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
payment_collection: undefined,
|
||||
}),
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
cart_id: testCart.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(
|
||||
`Failed to do something after updating payment collections`
|
||||
),
|
||||
},
|
||||
])
|
||||
expect(payCols.length).toEqual(0)
|
||||
|
||||
const updatedPaymentCollection =
|
||||
await paymentModule.retrievePaymentCollection(
|
||||
paymentCollection.id,
|
||||
{
|
||||
relations: ["payment_sessions"],
|
||||
}
|
||||
)
|
||||
|
||||
const sessions = await paymentModule.listPaymentSessions({
|
||||
payment_collection_id: paymentCollection.id,
|
||||
})
|
||||
|
||||
expect(sessions).toHaveLength(1)
|
||||
expect(sessions[0].id).not.toEqual(paymentSession.id)
|
||||
expect(sessions[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 5000,
|
||||
currency_code: "dkk",
|
||||
})
|
||||
)
|
||||
expect(updatedPaymentCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
id: paymentCollection.id,
|
||||
amount: 5000,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test:integration": "node --expose-gc ./../../node_modules/.bin/jest --ci --silent=true -i --detectOpenHandles --logHeapUsage --forceExit",
|
||||
"test:integration": "node --expose-gc ./../../node_modules/.bin/jest --ci --silent=false -i --detectOpenHandles --logHeapUsage --forceExit",
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
26
packages/core-flows/src/common/steps/use-remote-query.ts
Normal file
26
packages/core-flows/src/common/steps/use-remote-query.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
entry_point: string
|
||||
fields: string[]
|
||||
variables?: Record<string, any>
|
||||
}
|
||||
|
||||
export const useRemoteQueryStepId = "use-remote-query"
|
||||
export const useRemoteQueryStep = createStep(
|
||||
useRemoteQueryStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const query = container.resolve("remoteQuery")
|
||||
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: data.entry_point,
|
||||
fields: data.fields,
|
||||
variables: data.variables,
|
||||
})
|
||||
|
||||
const result = await query(queryObject)
|
||||
|
||||
return new StepResponse(result)
|
||||
}
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./add-to-cart"
|
||||
export * from "./create-carts"
|
||||
export * from "./create-payment-collection-for-cart"
|
||||
export * from "./refresh-payment-collection"
|
||||
export * from "./update-cart"
|
||||
export * from "./update-cart-promotions"
|
||||
export * from "./update-line-item-in-cart"
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
StepResponse,
|
||||
WorkflowData,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import {
|
||||
deletePaymentSessionStep,
|
||||
updatePaymentCollectionStep,
|
||||
} from "../../payment-collection"
|
||||
|
||||
type WorklowInput = {
|
||||
cart_id: string
|
||||
}
|
||||
|
||||
interface StepInput {
|
||||
cart_id: string
|
||||
}
|
||||
|
||||
// We export a step running the workflow too, so that we can use it as a subworkflow e.g. in the update cart workflows
|
||||
export const refreshPaymentCollectionForCartStepId =
|
||||
"refresh-payment-collection-for-cart"
|
||||
export const refreshPaymentCollectionForCartStep = createStep(
|
||||
refreshPaymentCollectionForCartStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
await refreshPaymentCollectionForCartWorkflow(container).run({
|
||||
input: {
|
||||
cart_id: data.cart_id,
|
||||
},
|
||||
})
|
||||
|
||||
return new StepResponse(null)
|
||||
}
|
||||
)
|
||||
|
||||
export const refreshPaymentCollectionForCartWorkflowId =
|
||||
"refresh-payment-collection-for-cart"
|
||||
export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
||||
refreshPaymentCollectionForCartWorkflowId,
|
||||
(input: WorkflowData<WorklowInput>): WorkflowData<void> => {
|
||||
const carts = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: [
|
||||
"id",
|
||||
"total",
|
||||
"currency_code",
|
||||
"payment_collection.id",
|
||||
"payment_collection.payment_sessions.id",
|
||||
],
|
||||
variables: { id: input.cart_id },
|
||||
})
|
||||
|
||||
deletePaymentSessionStep({
|
||||
payment_session_id: carts[0].payment_collection.payment_sessions?.[0].id,
|
||||
})
|
||||
|
||||
// TODO: Temporary fixed cart total, so we can test the workflow.
|
||||
// This will be removed when the totals utilities are built.
|
||||
const cartTotal = 4242
|
||||
|
||||
updatePaymentCollectionStep({
|
||||
selector: { id: carts[0].payment_collection.id },
|
||||
update: {
|
||||
amount: cartTotal,
|
||||
currency_code: carts[0].currency_code,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
updateCartsStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
|
||||
|
||||
export const updateCartWorkflowId = "update-cart"
|
||||
export const updateCartWorkflow = createWorkflow(
|
||||
@@ -68,6 +69,10 @@ export const updateCartWorkflow = createWorkflow(
|
||||
action: PromotionActions.REPLACE,
|
||||
})
|
||||
|
||||
refreshPaymentCollectionForCartStep({
|
||||
cart_id: input.id,
|
||||
})
|
||||
|
||||
const retrieveCartInput = {
|
||||
id: input.id,
|
||||
config: {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IPaymentModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
payment_session_id?: string
|
||||
}
|
||||
|
||||
export const deletePaymentSessionStepId = "delete-payment-session"
|
||||
export const deletePaymentSessionStep = createStep(
|
||||
deletePaymentSessionStepId,
|
||||
async (input: StepInput, { container }) => {
|
||||
const service = container.resolve<IPaymentModuleService>(
|
||||
ModuleRegistrationName.PAYMENT
|
||||
)
|
||||
|
||||
if (!input.payment_session_id) {
|
||||
return new StepResponse(void 0, null)
|
||||
}
|
||||
|
||||
const [session] = await service.listPaymentSessions({
|
||||
id: input.payment_session_id,
|
||||
})
|
||||
|
||||
await service.deletePaymentSession(input.payment_session_id)
|
||||
|
||||
return new StepResponse(input.payment_session_id, session)
|
||||
},
|
||||
async (input, { container }) => {
|
||||
const service = container.resolve<IPaymentModuleService>(
|
||||
ModuleRegistrationName.PAYMENT
|
||||
)
|
||||
|
||||
if (!input || !input.payment_collection) {
|
||||
return
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from "./create-payment-session"
|
||||
export * from "./delete-payment-session"
|
||||
export * from "./retrieve-payment-collection"
|
||||
export * from "./update-payment-collection"
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
FilterablePaymentCollectionProps,
|
||||
IPaymentModuleService,
|
||||
PaymentCollectionUpdatableFields,
|
||||
} from "@medusajs/types"
|
||||
import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
selector: FilterablePaymentCollectionProps
|
||||
update: PaymentCollectionUpdatableFields
|
||||
}
|
||||
|
||||
export const updatePaymentCollectionStepId = "update-payment-collection"
|
||||
export const updatePaymentCollectionStep = createStep(
|
||||
updatePaymentCollectionStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const paymentModuleService = container.resolve<IPaymentModuleService>(
|
||||
ModuleRegistrationName.PAYMENT
|
||||
)
|
||||
|
||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray([
|
||||
data.update,
|
||||
])
|
||||
|
||||
const prevData = await paymentModuleService.listPaymentCollections(
|
||||
data.selector,
|
||||
{
|
||||
select: selects,
|
||||
relations,
|
||||
}
|
||||
)
|
||||
|
||||
const updated = await paymentModuleService.updatePaymentCollections(
|
||||
data.selector,
|
||||
data.update
|
||||
)
|
||||
|
||||
return new StepResponse(updated, prevData)
|
||||
},
|
||||
async (prevData, { container }) => {
|
||||
if (!prevData) {
|
||||
return
|
||||
}
|
||||
const paymentModuleService = container.resolve<IPaymentModuleService>(
|
||||
ModuleRegistrationName.PAYMENT
|
||||
)
|
||||
|
||||
await paymentModuleService.upsertPaymentCollections(
|
||||
prevData.map((pc) => ({
|
||||
id: pc.id,
|
||||
amount: pc.amount,
|
||||
currency_code: pc.currency_code,
|
||||
metadata: pc.metadata,
|
||||
}))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { initModules, InitModulesOptions } from "./init-modules"
|
||||
import {
|
||||
MedusaAppOutput,
|
||||
MedusaModuleConfig,
|
||||
ModulesDefinition
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { getDatabaseURL, getMikroOrmWrapper, TestDatabase } from "./database"
|
||||
import { ContainerRegistrationKeys, ModulesSdkUtils } from "@medusajs/utils"
|
||||
import { TestDatabase, getDatabaseURL, getMikroOrmWrapper } from "./database"
|
||||
import { InitModulesOptions, initModules } from "./init-modules"
|
||||
|
||||
import { MockEventBusService } from "."
|
||||
|
||||
|
||||
@@ -277,8 +277,7 @@ moduleIntegrationTestRunner({
|
||||
|
||||
describe("update", () => {
|
||||
it("should update a Payment Collection", async () => {
|
||||
await service.updatePaymentCollections({
|
||||
id: "pay-col-id-2",
|
||||
await service.updatePaymentCollections("pay-col-id-2", {
|
||||
currency_code: "eur",
|
||||
region_id: "reg-2",
|
||||
})
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import { Payment, PaymentCollection, PaymentProvider } from "@models"
|
||||
import {
|
||||
Payment,
|
||||
PaymentCollection,
|
||||
PaymentProvider,
|
||||
PaymentSession,
|
||||
} from "@models"
|
||||
|
||||
export const LinkableKeys = {
|
||||
payment_id: Payment.name,
|
||||
@@ -37,6 +42,13 @@ export const joinerConfig: ModuleJoinerConfig = {
|
||||
entity: PaymentCollection.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["payment_session", "payment_sessions"],
|
||||
args: {
|
||||
entity: PaymentSession.name,
|
||||
methodSuffix: "PaymentSessions",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ["payment_provider", "payment_providers"],
|
||||
args: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export class Migration20240225134525 extends Migration {
|
||||
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 "deleted_at" TIMESTAMPTZ NULL;
|
||||
ALTER TABLE IF EXISTS "payment_session" ADD COLUMN IF NOT EXISTS "context" JSONB 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_collection_id" TEXT NOT NULL;
|
||||
@@ -161,6 +162,7 @@ export class Migration20240225134525 extends Migration {
|
||||
"raw_amount" JSONB NOT NULL,
|
||||
"provider_id" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"context" JSONB NULL,
|
||||
"status" TEXT CHECK ("status" IN ('authorized', 'pending', 'requires_more', 'error', 'canceled')) NOT NULL DEFAULT 'pending',
|
||||
"authorized_at" TIMESTAMPTZ NULL,
|
||||
"payment_collection_id" TEXT NOT NULL,
|
||||
|
||||
@@ -43,6 +43,9 @@ export default class PaymentSession {
|
||||
@Property({ columnType: "jsonb" })
|
||||
data: Record<string, unknown> = {}
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
context: Record<string, unknown> | null
|
||||
|
||||
@Enum({
|
||||
items: () => PaymentSessionStatus,
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CreatePaymentSessionDTO,
|
||||
CreateRefundDTO,
|
||||
DAL,
|
||||
FilterablePaymentCollectionProps,
|
||||
FilterablePaymentProviderProps,
|
||||
FindConfig,
|
||||
InternalModuleDeclaration,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
PaymentCollectionDTO,
|
||||
PaymentCollectionUpdatableFields,
|
||||
PaymentDTO,
|
||||
PaymentProviderDTO,
|
||||
PaymentSessionDTO,
|
||||
@@ -22,14 +24,17 @@ import {
|
||||
UpdatePaymentCollectionDTO,
|
||||
UpdatePaymentDTO,
|
||||
UpdatePaymentSessionDTO,
|
||||
UpsertPaymentCollectionDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isString,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
PaymentActions,
|
||||
promiseAll,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
Capture,
|
||||
@@ -127,15 +132,14 @@ export default class PaymentModuleService<
|
||||
data: CreatePaymentCollectionDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
@InjectManager("baseRepository_")
|
||||
async createPaymentCollections(
|
||||
data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[],
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
|
||||
const collections = await this.paymentCollectionService_.create(
|
||||
const collections = await this.createPaymentCollections_(
|
||||
input,
|
||||
sharedContext
|
||||
)
|
||||
@@ -148,23 +152,54 @@ export default class PaymentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async createPaymentCollections_(
|
||||
data: CreatePaymentCollectionDTO[],
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentCollection[]> {
|
||||
return this.paymentCollectionService_.create(data, sharedContext)
|
||||
}
|
||||
|
||||
updatePaymentCollections(
|
||||
data: UpdatePaymentCollectionDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
updatePaymentCollections(
|
||||
data: UpdatePaymentCollectionDTO,
|
||||
paymentCollectionId: string,
|
||||
data: PaymentCollectionUpdatableFields,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO>
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async updatePaymentCollections(
|
||||
data: UpdatePaymentCollectionDTO | UpdatePaymentCollectionDTO[],
|
||||
updatePaymentCollections(
|
||||
selector: FilterablePaymentCollectionProps,
|
||||
data: PaymentCollectionUpdatableFields,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
@InjectManager("baseRepository_")
|
||||
async updatePaymentCollections(
|
||||
idOrSelector: string | FilterablePaymentCollectionProps,
|
||||
data: PaymentCollectionUpdatableFields,
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
const result = await this.paymentCollectionService_.update(
|
||||
input,
|
||||
let updateData: UpdatePaymentCollectionDTO[] = []
|
||||
|
||||
if (isString(idOrSelector)) {
|
||||
updateData = [
|
||||
{
|
||||
id: idOrSelector,
|
||||
...data,
|
||||
},
|
||||
]
|
||||
} else {
|
||||
const collections = await this.paymentCollectionService_.list(
|
||||
idOrSelector,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
updateData = collections.map((c) => ({
|
||||
id: c.id,
|
||||
...data,
|
||||
}))
|
||||
}
|
||||
|
||||
const result = await this.updatePaymentCollections_(
|
||||
updateData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -176,6 +211,52 @@ export default class PaymentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async updatePaymentCollections_(
|
||||
data: UpdatePaymentCollectionDTO[],
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentCollection[]> {
|
||||
return await this.paymentCollectionService_.update(data, sharedContext)
|
||||
}
|
||||
|
||||
upsertPaymentCollections(
|
||||
data: UpsertPaymentCollectionDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
upsertPaymentCollections(
|
||||
data: UpsertPaymentCollectionDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO>
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async upsertPaymentCollections(
|
||||
data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[],
|
||||
@MedusaContext() sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
|
||||
const input = Array.isArray(data) ? data : [data]
|
||||
const forUpdate = input.filter(
|
||||
(collection): collection is UpdatePaymentCollectionDTO => !!collection.id
|
||||
)
|
||||
const forCreate = input.filter(
|
||||
(collection): collection is CreatePaymentCollectionDTO => !collection.id
|
||||
)
|
||||
|
||||
const operations: Promise<PaymentCollection[]>[] = []
|
||||
|
||||
if (forCreate.length) {
|
||||
operations.push(this.createPaymentCollections_(forCreate, sharedContext))
|
||||
}
|
||||
if (forUpdate.length) {
|
||||
operations.push(this.updatePaymentCollections_(forUpdate, sharedContext))
|
||||
}
|
||||
|
||||
const result = (await promiseAll(operations)).flat()
|
||||
|
||||
return await this.baseRepository_.serialize<
|
||||
PaymentCollectionDTO[] | PaymentCollectionDTO
|
||||
>(Array.isArray(data) ? result : result[0])
|
||||
}
|
||||
|
||||
completePaymentCollections(
|
||||
paymentCollectionId: string,
|
||||
sharedContext?: Context
|
||||
|
||||
@@ -114,7 +114,7 @@ export interface PaymentCollectionDTO {
|
||||
/**
|
||||
* Holds custom data in key-value pairs
|
||||
*/
|
||||
metadata?: Record<string, unknown> | null
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The status of the payment collection
|
||||
@@ -417,6 +417,11 @@ export interface PaymentSessionDTO {
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Payment session context
|
||||
*/
|
||||
context?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* The status of the payment session
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PaymentCollectionStatus } from "./common"
|
||||
import { PaymentProviderContext } from "./provider"
|
||||
|
||||
/**
|
||||
@@ -35,21 +34,24 @@ export interface UpdatePaymentCollectionDTO
|
||||
* The ID of the payment collection.
|
||||
*/
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The authorized amount of the payment collection.
|
||||
*/
|
||||
authorized_amount?: number
|
||||
export interface UpsertPaymentCollectionDTO {
|
||||
id?: string
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
amount?: number
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The refunded amount of the payment collection.
|
||||
*/
|
||||
refunded_amount?: number
|
||||
|
||||
/**
|
||||
* The status of the payment collection.
|
||||
*/
|
||||
status?: PaymentCollectionStatus
|
||||
/**
|
||||
* The attributes to update in the payment collection.
|
||||
*/
|
||||
export interface PaymentCollectionUpdatableFields {
|
||||
region_id?: string
|
||||
currency_code?: string
|
||||
amount?: number
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,10 +20,11 @@ import {
|
||||
CreatePaymentCollectionDTO,
|
||||
CreatePaymentSessionDTO,
|
||||
CreateRefundDTO,
|
||||
PaymentCollectionUpdatableFields,
|
||||
ProviderWebhookPayload,
|
||||
UpdatePaymentCollectionDTO,
|
||||
UpdatePaymentDTO,
|
||||
UpdatePaymentSessionDTO,
|
||||
UpsertPaymentCollectionDTO,
|
||||
} from "./mutations"
|
||||
|
||||
/**
|
||||
@@ -164,33 +165,23 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<[PaymentCollectionDTO[], number]>
|
||||
|
||||
/**
|
||||
* This method updates existing payment collections.
|
||||
*
|
||||
* @param {UpdatePaymentCollectionDTO[]} data - The attributes to update in payment collections.
|
||||
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
|
||||
* @returns {Promise<PaymentCollectionDTO[]>} The updated payment collections.
|
||||
*
|
||||
* @example
|
||||
* {example-code}
|
||||
*/
|
||||
updatePaymentCollections(
|
||||
data: UpdatePaymentCollectionDTO[],
|
||||
paymentCollectionId: string,
|
||||
data: PaymentCollectionUpdatableFields,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO>
|
||||
updatePaymentCollections(
|
||||
selector: FilterablePaymentCollectionProps,
|
||||
data: PaymentCollectionUpdatableFields,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
|
||||
/**
|
||||
* This method updates an existing payment collection.
|
||||
*
|
||||
* @param {UpdatePaymentCollectionDTO} data - The attributes to update in a payment collection.
|
||||
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
|
||||
* @returns {Promise<PaymentCollectionDTO[]>} The updated payment collection.
|
||||
*
|
||||
* @example
|
||||
* {example-code}
|
||||
*/
|
||||
updatePaymentCollections(
|
||||
data: UpdatePaymentCollectionDTO,
|
||||
upsertPaymentCollections(
|
||||
data: UpsertPaymentCollectionDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO[]>
|
||||
upsertPaymentCollections(
|
||||
data: UpsertPaymentCollectionDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentCollectionDTO>
|
||||
|
||||
|
||||
74
packages/utils/src/common/__tests__/deep-copy.spec.ts
Normal file
74
packages/utils/src/common/__tests__/deep-copy.spec.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { deepCopy } from "../deep-copy"
|
||||
|
||||
class TestA {
|
||||
prop1: any
|
||||
prop2: any
|
||||
|
||||
constructor(prop1: any, prop2: any) {
|
||||
this.prop1 = prop1
|
||||
this.prop2 = prop2
|
||||
}
|
||||
}
|
||||
|
||||
class TestWrapper {
|
||||
prop1: any
|
||||
prop2: any
|
||||
|
||||
constructor(prop1: any, prop2: any) {
|
||||
this.prop1 = prop1
|
||||
this.prop2 = prop2
|
||||
}
|
||||
|
||||
factory() {
|
||||
return new TestA(deepCopy(this.prop1), deepCopy(this.prop2))
|
||||
}
|
||||
}
|
||||
|
||||
class TestWrapperWithoutDeepCopy {
|
||||
prop1: any
|
||||
prop2: any
|
||||
|
||||
constructor(prop1: any, prop2: any) {
|
||||
this.prop1 = prop1
|
||||
this.prop2 = prop2
|
||||
}
|
||||
|
||||
factory() {
|
||||
return new TestA(this.prop1, this.prop2)
|
||||
}
|
||||
}
|
||||
|
||||
describe("deepCopy", () => {
|
||||
it("should deep copy an object", () => {
|
||||
const prop1 = {
|
||||
prop1: 1,
|
||||
}
|
||||
|
||||
const prop2 = {
|
||||
prop1: 3,
|
||||
}
|
||||
|
||||
const wrapperWithoutDeepCopy = new TestWrapperWithoutDeepCopy(prop1, prop2)
|
||||
let factory1 = wrapperWithoutDeepCopy.factory()
|
||||
let factory2 = wrapperWithoutDeepCopy.factory()
|
||||
|
||||
factory1.prop1.prop1 = 2
|
||||
|
||||
expect(wrapperWithoutDeepCopy.prop1).toEqual({ prop1: 2 })
|
||||
expect(factory1.prop1).toEqual({ prop1: 2 })
|
||||
expect(factory2.prop1).toEqual({ prop1: 2 })
|
||||
|
||||
prop1.prop1 = 4
|
||||
prop2.prop1 = 4
|
||||
|
||||
const wrapper = new TestWrapper(prop1, prop2)
|
||||
factory1 = wrapper.factory()
|
||||
factory2 = wrapper.factory()
|
||||
|
||||
factory1.prop1.prop1 = 2
|
||||
|
||||
expect(wrapper.prop1).toEqual({ prop1: 4 })
|
||||
expect(factory1.prop1).toEqual({ prop1: 2 })
|
||||
expect(factory2.prop1).toEqual({ prop1: 4 })
|
||||
})
|
||||
})
|
||||
40
packages/utils/src/common/deep-copy.ts
Normal file
40
packages/utils/src/common/deep-copy.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { isObject } from "./is-object"
|
||||
|
||||
/**
|
||||
* In most casees, JSON.parse(JSON.stringify(obj)) is enough to deep copy an object.
|
||||
* But in some cases, it's not enough. For example, if the object contains a function or a proxy, it will be lost after JSON.parse(JSON.stringify(obj)).
|
||||
* Furthermore, structuredClone is not present in all environments, such as with jest so we need to use a custom deepCopy function.
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
export function deepCopy<T extends Record<any, any> = Record<any, any>>(
|
||||
obj: T | T[]
|
||||
): T | T[] {
|
||||
if (typeof structuredClone != "undefined") {
|
||||
return structuredClone(obj)
|
||||
}
|
||||
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
const copy: any[] = []
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
copy[i] = deepCopy(obj[i])
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
const copy: Record<any, any> = {}
|
||||
for (let attr in obj) {
|
||||
if (obj.hasOwnProperty(attr)) {
|
||||
copy[attr] = deepCopy(obj[attr])
|
||||
}
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export * from "./create-container-like"
|
||||
export * from "./create-psql-index-helper"
|
||||
export * from "./deduplicate"
|
||||
export * from "./deep-equal-obj"
|
||||
export * from "./deep-copy"
|
||||
export * from "./errors"
|
||||
export * from "./generate-entity-id"
|
||||
export * from "./generate-linkable-keys-map"
|
||||
|
||||
@@ -149,7 +149,7 @@ export const mikroOrmSerializer = async <TOutput extends object>(
|
||||
): Promise<TOutput> => {
|
||||
options ??= {}
|
||||
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
const data_ = (Array.isArray(data) ? data : [data]).filter(Boolean)
|
||||
|
||||
const forSerialization: unknown[] = []
|
||||
const notForSerialization: unknown[] = []
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrchestrationUtils, promiseAll } from "@medusajs/utils"
|
||||
import { OrchestrationUtils, deepCopy, promiseAll } from "@medusajs/utils"
|
||||
|
||||
async function resolveProperty(property, transactionContext) {
|
||||
const { invoke: invokeRes } = transactionContext
|
||||
@@ -31,6 +31,8 @@ async function resolveProperty(property, transactionContext) {
|
||||
* @internal
|
||||
*/
|
||||
export async function resolveValue(input, transactionContext) {
|
||||
const copiedInput = deepCopy(input)
|
||||
|
||||
const unwrapInput = async (
|
||||
inputTOUnwrap: Record<string, unknown>,
|
||||
parentRef: any
|
||||
@@ -63,9 +65,9 @@ export async function resolveValue(input, transactionContext) {
|
||||
return parentRef
|
||||
}
|
||||
|
||||
const result = input?.__type
|
||||
? await resolveProperty(input, transactionContext)
|
||||
: await unwrapInput(input, {})
|
||||
const result = copiedInput?.__type
|
||||
? await resolveProperty(copiedInput, transactionContext)
|
||||
: await unwrapInput(copiedInput, {})
|
||||
|
||||
return result && JSON.parse(JSON.stringify(result))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user