feat: Add payment collection creation for cart (#6527)

This commit is contained in:
Oli Juhl
2024-03-04 09:02:01 +01:00
committed by GitHub
parent 71ed21de4a
commit 883cb0dca7
11 changed files with 371 additions and 8 deletions

View File

@@ -1,9 +1,11 @@
import {
addToCartWorkflow,
createCartWorkflow,
createPaymentCollectionForCartWorkflow,
deleteLineItemsStepId,
deleteLineItemsWorkflow,
findOrCreateCustomerStepId,
linkCartAndPaymentCollectionsStepId,
updateLineItemInCartWorkflow,
updateLineItemsStepId,
} from "@medusajs/core-flows"
@@ -11,6 +13,7 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
ICartModuleService,
ICustomerModuleService,
IPaymentModuleService,
IPricingModuleService,
IProductModuleService,
IRegionModuleService,
@@ -37,7 +40,8 @@ describe("Carts workflows", () => {
let customerModule: ICustomerModuleService
let productModule: IProductModuleService
let pricingModule: IPricingModuleService
let remoteLink
let paymentModule: IPaymentModuleService
let remoteLink, remoteQuery
let defaultRegion
@@ -52,7 +56,9 @@ describe("Carts workflows", () => {
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT)
remoteLink = appContainer.resolve("remoteLink")
remoteQuery = appContainer.resolve("remoteQuery")
})
afterAll(async () => {
@@ -668,4 +674,142 @@ describe("Carts workflows", () => {
})
})
})
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",
})
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)
})
})
})
})

View File

@@ -669,4 +669,35 @@ describe("Store Carts API", () => {
)
})
})
describe("POST /store/carts/:id/payment-collections", () => {
it("should create a payment collection for the cart", async () => {
const region = await regionModuleService.create({
name: "US",
currency_code: "usd",
})
const cart = await cartModuleService.create({
currency_code: "usd",
region_id: region.id,
})
const api = useApi() as any
const response = await api.post(
`/store/carts/${cart.id}/payment-collections`
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
id: cart.id,
currency_code: "usd",
payment_collection: expect.objectContaining({
id: expect.any(String),
amount: 0,
}),
})
)
})
})
})

View File

@@ -0,0 +1,38 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPaymentModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type StepInput = {
region_id: string
currency_code: string
amount: number
metadata?: Record<string, unknown>
}
export const createPaymentCollectionsStepId = "create-payment-collections"
export const createPaymentCollectionsStep = createStep(
createPaymentCollectionsStepId,
async (data: StepInput[], { container }) => {
const service = container.resolve<IPaymentModuleService>(
ModuleRegistrationName.PAYMENT
)
const created = await service.createPaymentCollections(data)
return new StepResponse(
created,
created.map((collection) => collection.id)
)
},
async (createdIds, { container }) => {
if (!createdIds?.length) {
return
}
const service = container.resolve<IPaymentModuleService>(
ModuleRegistrationName.PAYMENT
)
await service.deletePaymentCollections(createdIds)
}
)

View File

@@ -0,0 +1,41 @@
import { Modules } from "@medusajs/modules-sdk"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type StepInput = {
links: {
cart_id: string
payment_collection_id: string
}[]
}
export const linkCartAndPaymentCollectionsStepId =
"link-cart-payment-collection"
export const linkCartAndPaymentCollectionsStep = createStep(
linkCartAndPaymentCollectionsStepId,
async (data: StepInput, { container }) => {
const remoteLink = container.resolve("remoteLink")
const links = data.links.map((d) => ({
[Modules.CART]: { cart_id: d.cart_id },
[Modules.PAYMENT]: { payment_collection_id: d.payment_collection_id },
}))
await remoteLink.create(links)
return new StepResponse(void 0, data)
},
async (data, { container }) => {
if (!data) {
return
}
const remoteLink = container.resolve("remoteLink")
const links = data.links.map((d) => ({
[Modules.CART]: { cart_id: d.cart_id },
[Modules.PAYMENT]: { payment_collection_id: d.payment_collection_id },
}))
await remoteLink.dismiss(links)
}
)

View File

@@ -4,7 +4,7 @@ import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput {
id: string
config: FindConfig<CartDTO>
config?: FindConfig<CartDTO>
}
export const retrieveCartStepId = "retrieve-cart"

View File

@@ -0,0 +1,38 @@
import {
CartDTO,
CreatePaymentCollectionForCartWorkflowInputDTO,
} from "@medusajs/types"
import {
WorkflowData,
createWorkflow,
transform,
} from "@medusajs/workflows-sdk"
import { retrieveCartStep } from "../steps"
import { createPaymentCollectionsStep } from "../steps/create-payment-collection"
import { linkCartAndPaymentCollectionsStep } from "../steps/link-cart-payment-collection"
export const createPaymentCollectionForCartWorkflowId =
"create-payment-collection-for-cart"
export const createPaymentCollectionForCartWorkflow = createWorkflow(
createPaymentCollectionForCartWorkflowId,
(
input: WorkflowData<CreatePaymentCollectionForCartWorkflowInputDTO>
): WorkflowData<CartDTO> => {
const created = createPaymentCollectionsStep([input])
const link = transform({ cartId: input.cart_id, created }, (data) => ({
links: [
{
cart_id: data.cartId,
payment_collection_id: data.created[0].id,
},
],
}))
linkCartAndPaymentCollectionsStep(link)
const cart = retrieveCartStep({ id: input.cart_id })
return cart
}
)

View File

@@ -1,6 +1,6 @@
export * from "./add-to-cart"
export * from "./create-carts"
export * from "./create-payment-collection-for-cart"
export * from "./update-cart"
export * from "./update-cart-promotions"
export * from "./update-line-item-in-cart"

View File

@@ -0,0 +1,50 @@
import { createPaymentCollectionForCartWorkflow } from "@medusajs/core-flows"
import { UpdateCartDataDTO } from "@medusajs/types"
import { remoteQueryObjectFromString } from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
import { defaultStoreCartFields } from "../../query-config"
export const POST = async (
req: MedusaRequest<UpdateCartDataDTO>,
res: MedusaResponse
) => {
const workflow = createPaymentCollectionForCartWorkflow(req.scope)
const remoteQuery = req.scope.resolve("remoteQuery")
let [cart] = await remoteQuery(
{
cart: {
fields: ["id", "currency_code", "region_id", "total"],
},
},
{
cart: { id: req.params.id },
}
)
const { errors } = await workflow.run({
input: {
cart_id: req.params.id,
region_id: cart.region_id,
currency_code: cart.currency_code,
amount: cart.total ?? 0, // TODO: This should be calculated from the cart when totals decoration is introduced
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const query = remoteQueryObjectFromString({
entryPoint: "cart",
variables: { id: req.params.id },
fields: defaultStoreCartFields,
})
;[cart] = await remoteQuery(query)
res.status(200).json({ cart })
}

View File

@@ -42,6 +42,11 @@ export const defaultStoreCartFields = [
"region.name",
"region.currency_code",
"sales_channel_id",
// TODO: To be updated when payment sessions are introduces in the Rest API
"payment_collection.id",
"payment_collection.amount",
"payment_collection.payment_sessions",
]
export const defaultStoreCartRelations = [

View File

@@ -23,10 +23,18 @@ export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PAYMENT,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: {
name: ["payment", "payments"],
args: {
entity: Payment.name,
alias: [
{
name: ["payment", "payments"],
args: {
entity: Payment.name,
},
},
},
{
name: ["payment_collection", "payment_collections"],
args: {
entity: PaymentCollection.name,
},
},
],
}

View File

@@ -83,3 +83,11 @@ export interface UpdateCartWorkflowInputDTO {
currency_code?: string
metadata?: Record<string, unknown> | null
}
export interface CreatePaymentCollectionForCartWorkflowInputDTO {
cart_id: string
region_id: string
currency_code: string
amount: number
metadata?: Record<string, unknown>
}