feat: Create cart with line items (#6449)
**What** - Add support for creating a cart with items - Add endpoint `POST /store/carts/:id/line-items` - Add `CreateCartWorkflow` - Add `AddToCartWorkflow` - Add steps for both workflows **Testing** - Endpoints - Workflows I would still call this a first iteration, as we are missing a few pieces of the full flow, such as payment sessions, discounts, and taxes. Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
co-authored by
Adrien de Peretti
parent
ac86362e81
commit
7ebe885ec9
@@ -0,0 +1,31 @@
|
||||
import { CreateLineItemForCartDTO, ICartModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import { ModuleRegistrationName } from "../../../../../modules-sdk/dist"
|
||||
|
||||
interface StepInput {
|
||||
items: CreateLineItemForCartDTO[]
|
||||
}
|
||||
|
||||
export const addToCartStepId = "add-to-cart-step"
|
||||
export const addToCartStep = createStep(
|
||||
addToCartStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const cartService = container.resolve<ICartModuleService>(
|
||||
ModuleRegistrationName.CART
|
||||
)
|
||||
|
||||
const items = await cartService.addLineItems(data.items)
|
||||
|
||||
return new StepResponse(items)
|
||||
},
|
||||
async (createdLineItems, { container }) => {
|
||||
const cartService: ICartModuleService = container.resolve(
|
||||
ModuleRegistrationName.CART
|
||||
)
|
||||
if (!createdLineItems?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
await cartService.removeLineItems(createdLineItems.map((c) => c.id))
|
||||
}
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IRegionModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
export const findOneOrAnyRegionStepId = "find-one-or-any-region"
|
||||
@@ -14,7 +15,10 @@ export const findOneOrAnyRegionStep = createStep(
|
||||
const regions = await service.list({})
|
||||
|
||||
if (!regions?.length) {
|
||||
throw Error("No regions found")
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"No regions found"
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(regions[0])
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IPricingModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
variantIds: string[]
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const getVariantPriceSetsStepId = "get-variant-price-sets"
|
||||
export const getVariantPriceSetsStep = createStep(
|
||||
getVariantPriceSetsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
if (!data.variantIds.length) {
|
||||
return new StepResponse({})
|
||||
}
|
||||
|
||||
const pricingModuleService = container.resolve<IPricingModuleService>(
|
||||
ModuleRegistrationName.PRICING
|
||||
)
|
||||
|
||||
const remoteQuery = container.resolve("remoteQuery")
|
||||
|
||||
const variantPriceSets = await remoteQuery(
|
||||
{
|
||||
variant: {
|
||||
fields: ["id"],
|
||||
price: {
|
||||
fields: ["price_set_id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: {
|
||||
id: data.variantIds,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const notFound: string[] = []
|
||||
const priceSetIds: string[] = []
|
||||
|
||||
variantPriceSets.forEach((v) => {
|
||||
if (v.price?.price_set_id) {
|
||||
priceSetIds.push(v.price.price_set_id)
|
||||
} else {
|
||||
notFound.push(v.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (notFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${notFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const calculatedPriceSets = await pricingModuleService.calculatePrices(
|
||||
{ id: priceSetIds },
|
||||
{ context: data.context as Record<string, string | number> }
|
||||
)
|
||||
|
||||
const idToPriceSet = new Map<string, Record<string, any>>(
|
||||
calculatedPriceSets.map((p) => [p.id, p])
|
||||
)
|
||||
|
||||
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
||||
(acc, { id, price }) => {
|
||||
const calculatedPriceSet = idToPriceSet.get(price?.price_set_id)
|
||||
if (calculatedPriceSet) {
|
||||
acc[id] = calculatedPriceSet
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
return new StepResponse(variantToCalculatedPriceSets)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
FilterableProductVariantProps,
|
||||
FindConfig,
|
||||
IProductModuleService,
|
||||
ProductVariantDTO,
|
||||
} from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
filter?: FilterableProductVariantProps
|
||||
config?: FindConfig<ProductVariantDTO>
|
||||
}
|
||||
|
||||
export const getVariantsStepId = "get-variants"
|
||||
export const getVariantsStep = createStep(
|
||||
getVariantsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const productModuleService = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
const variants = await productModuleService.listVariants(
|
||||
data.filter,
|
||||
data.config
|
||||
)
|
||||
|
||||
return new StepResponse(variants)
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./add-to-cart"
|
||||
export * from "./create-carts"
|
||||
export * from "./create-line-item-adjustments"
|
||||
export * from "./create-shipping-method-adjustments"
|
||||
@@ -5,8 +6,12 @@ export * from "./find-one-or-any-region"
|
||||
export * from "./find-or-create-customer"
|
||||
export * from "./find-sales-channel"
|
||||
export * from "./get-actions-to-compute-from-promotions"
|
||||
export * from "./get-variant-price-sets"
|
||||
export * from "./get-variants"
|
||||
export * from "./prepare-adjustments-from-promotion-actions"
|
||||
export * from "./remove-line-item-adjustments"
|
||||
export * from "./remove-shipping-method-adjustments"
|
||||
export * from "./retrieve-cart"
|
||||
export * from "./update-carts"
|
||||
export * from "./validate-variants-existence"
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
variantIds: string[]
|
||||
}
|
||||
|
||||
export const validateVariantsExistStepId = "validate-variants-exist"
|
||||
export const validateVariantsExistStep = createStep(
|
||||
validateVariantsExistStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const productModuleService = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
const variants = await productModuleService.listVariants(
|
||||
{
|
||||
id: data.variantIds,
|
||||
},
|
||||
{
|
||||
select: ["id"],
|
||||
}
|
||||
)
|
||||
|
||||
const variantIdToData = new Set(variants.map((v) => v.id))
|
||||
|
||||
const notFoundVariants = new Set(
|
||||
[...data.variantIds].filter((x) => !variantIdToData.has(x))
|
||||
)
|
||||
|
||||
if (notFoundVariants.size) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${[...notFoundVariants].join(", ")} do not exist`
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(Array.from(variants.map((v) => v.id)))
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ProductVariantDTO } from "@medusajs/types"
|
||||
|
||||
interface Input {
|
||||
quantity: number
|
||||
metadata?: Record<string, any>
|
||||
unitPrice: number
|
||||
variant: ProductVariantDTO
|
||||
cartId?: string
|
||||
}
|
||||
|
||||
export function prepareLineItemData(data: Input) {
|
||||
const { variant, unitPrice, quantity, metadata, cartId } = data
|
||||
const lineItem: any = {
|
||||
quantity,
|
||||
title: variant.title,
|
||||
|
||||
subtitle: variant.product.title,
|
||||
thumbnail: variant.product.thumbnail,
|
||||
|
||||
product_id: variant.product.id,
|
||||
product_title: variant.product.title,
|
||||
product_description: variant.product.description,
|
||||
product_subtitle: variant.product.subtitle,
|
||||
product_type: variant.product.type?.[0].value ?? null,
|
||||
product_collection: variant.product.collection?.[0].value ?? null,
|
||||
product_handle: variant.product.handle,
|
||||
|
||||
variant_id: variant.id,
|
||||
variant_sku: variant.sku,
|
||||
variant_barcode: variant.barcode,
|
||||
variant_title: variant.title,
|
||||
|
||||
unit_price: unitPrice,
|
||||
metadata,
|
||||
}
|
||||
|
||||
if (cartId) {
|
||||
lineItem.cart_id = cartId
|
||||
}
|
||||
|
||||
return lineItem
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
AddToCartWorkflowInputDTO,
|
||||
CreateLineItemForCartDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
addToCartStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
validateVariantsExistStep,
|
||||
} from "../steps"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
|
||||
// TODO: The AddToCartWorkflow are missing the following steps:
|
||||
// - Confirm inventory exists (inventory module)
|
||||
// - Refresh/delete shipping methods (fulfillment module)
|
||||
// - Create line item adjustments (promotion module)
|
||||
// - Update payment sessions (payment module)
|
||||
|
||||
export const addToCartWorkflowId = "add-to-cart"
|
||||
export const addToCartWorkflow = createWorkflow(
|
||||
addToCartWorkflowId,
|
||||
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
})
|
||||
|
||||
validateVariantsExistStep({ variantIds })
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform({ cart: input.cart }, (data) => {
|
||||
return {
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
customer_id: data.cart.customer_id,
|
||||
}
|
||||
})
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
|
||||
const variants = getVariantsStep({
|
||||
filter: { id: variantIds },
|
||||
})
|
||||
|
||||
const lineItems = transform(
|
||||
{ priceSets, input, variants, cart: input.cart },
|
||||
(data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
|
||||
return prepareLineItemData({
|
||||
variant: variant,
|
||||
unitPrice: data.priceSets[item.variant_id].calculated_amount,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
cartId: data.cart.id,
|
||||
}) as CreateLineItemForCartDTO
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
)
|
||||
|
||||
const items = addToCartStep({ items: lineItems })
|
||||
|
||||
return items
|
||||
}
|
||||
)
|
||||
@@ -1,21 +1,35 @@
|
||||
import { CartDTO, CreateCartWorkflowInputDTO } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import {
|
||||
createCartsStep,
|
||||
findOneOrAnyRegionStep,
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
validateVariantsExistStep,
|
||||
} from "../steps"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
|
||||
// TODO: The UpdateLineItemsWorkflow are missing the following steps:
|
||||
// - Confirm inventory exists (inventory module)
|
||||
// - Refresh/delete shipping methods (fulfillment module)
|
||||
// - Refresh/create line item adjustments (promotion module)
|
||||
// - Update payment sessions (payment module)
|
||||
|
||||
export const createCartWorkflowId = "create-cart"
|
||||
export const createCartWorkflow = createWorkflow(
|
||||
createCartWorkflowId,
|
||||
(input: WorkflowData<CreateCartWorkflowInputDTO>): WorkflowData<CartDTO> => {
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
})
|
||||
|
||||
const [salesChannel, region, customerData] = parallelize(
|
||||
findSalesChannelStep({
|
||||
salesChannelId: input.sales_channel_id,
|
||||
@@ -26,9 +40,27 @@ export const createCartWorkflow = createWorkflow(
|
||||
findOrCreateCustomerStep({
|
||||
customerId: input.customer_id,
|
||||
email: input.email,
|
||||
})
|
||||
}),
|
||||
validateVariantsExistStep({ variantIds })
|
||||
)
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform(
|
||||
{ input, region, customerData },
|
||||
(data) => {
|
||||
return {
|
||||
currency_code: data.input.currency_code ?? data.region.currency_code,
|
||||
region_id: data.region.id,
|
||||
customer_id: data.customerData.customer?.id,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
|
||||
const cartInput = transform(
|
||||
{ input, region, customerData, salesChannel },
|
||||
(data) => {
|
||||
@@ -51,11 +83,53 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: Add line items
|
||||
const variants = getVariantsStep({
|
||||
filter: { id: variantIds },
|
||||
config: {
|
||||
select: [
|
||||
"id",
|
||||
"title",
|
||||
"sku",
|
||||
"barcode",
|
||||
"product.id",
|
||||
"product.title",
|
||||
"product.description",
|
||||
"product.subtitle",
|
||||
"product.thumbnail",
|
||||
"product.type",
|
||||
"product.collection",
|
||||
"product.handle",
|
||||
],
|
||||
relations: ["product"],
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const cart = createCartsStep([cartInput])
|
||||
const lineItems = transform({ priceSets, input, variants }, (data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
|
||||
return cart[0]
|
||||
return prepareLineItemData({
|
||||
variant: variant,
|
||||
unitPrice: data.priceSets[item.variant_id].calculated_amount,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
})
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const cartToCreate = transform({ lineItems, cartInput }, (data) => {
|
||||
return {
|
||||
...data.cartInput,
|
||||
items: data.lineItems,
|
||||
}
|
||||
})
|
||||
|
||||
const carts = createCartsStep([cartToCreate])
|
||||
|
||||
const cart = transform({ carts }, (data) => data.carts?.[0])
|
||||
|
||||
return cart
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./add-to-cart"
|
||||
export * from "./create-carts"
|
||||
export * from "./update-cart-promotions"
|
||||
export * from "./update-carts"
|
||||
|
||||
Reference in New Issue
Block a user