feat(medusa,workflows) Create cart workflow (#4685)
* chore: add baseline test for create cart * chore: add basic paths into handlers + make first tests pass * chore: move input alias to cart specific workflow * chore: move data around into buckets * chore: normalize handlers and introduce types * chore: move aliases to handlers concern * chore: add compensation step for create cart * chore: merge with latest develop * chore: handle error manually + type inputs * chore: handle error manually * chore: added types for each handler * chore: remove addresses * chore: added changset * chore: undo package changes * chore: added config settings to retreieve, cleanup of types * chore: capitalize cart handlers * chore: rename todo * chore: add feature flag for workflow * chore: reorder handlers * chore: add logger to route handler * chore: removed weird vscode moving around things * chore: refactor handlers * chore: refactor compensate step * chore: changed poistion * chore: aggregate config data * chore: moved handlers to their own domain + pr review addressing * chore: address pr reviews * chore: move types to type package * chore: update type to include config * chore: remove error scoping
This commit is contained in:
7
.changeset/shy-kiwis-jog.md
Normal file
7
.changeset/shy-kiwis-jog.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/workflows": patch
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(medusa,workflows,types) Create cart workflow
|
||||
210
integration-tests/plugins/__tests__/cart/store/index.ts
Normal file
210
integration-tests/plugins/__tests__/cart/store/index.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { MoneyAmount, PriceList, Region } from "@medusajs/medusa"
|
||||
import path from "path"
|
||||
|
||||
import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||
import setupServer from "../../../../environment-helpers/setup-server"
|
||||
import { setPort, useApi } from "../../../../environment-helpers/use-api"
|
||||
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||
import { simpleProductFactory } from "../../../../factories"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
describe("/store/carts", () => {
|
||||
let medusaProcess
|
||||
let dbConnection
|
||||
let express
|
||||
|
||||
const doAfterEach = async () => {
|
||||
const db = useDb()
|
||||
return await db.teardown()
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
dbConnection = await initDb({ cwd })
|
||||
medusaProcess = await setupServer({ cwd, verbose: true })
|
||||
const { app, port } = await bootstrapApp({ cwd })
|
||||
setPort(port)
|
||||
express = app.listen(port, () => {
|
||||
process.send?.(port)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /store/carts", () => {
|
||||
let prod1
|
||||
let prodSale
|
||||
|
||||
beforeEach(async () => {
|
||||
const manager = dbConnection.manager
|
||||
await manager.insert(Region, {
|
||||
id: "region",
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
tax_rate: 0,
|
||||
})
|
||||
|
||||
await manager.query(
|
||||
`UPDATE "country"
|
||||
SET region_id='region'
|
||||
WHERE iso_2 = 'us'`
|
||||
)
|
||||
|
||||
prod1 = await simpleProductFactory(dbConnection, {
|
||||
id: "test-product",
|
||||
variants: [{ id: "test-variant_1" }],
|
||||
})
|
||||
|
||||
prodSale = await simpleProductFactory(dbConnection, {
|
||||
id: "test-product-sale",
|
||||
variants: [
|
||||
{
|
||||
id: "test-variant-sale",
|
||||
prices: [{ amount: 1000, currency: "usd" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await doAfterEach()
|
||||
})
|
||||
|
||||
it("should create a cart", async () => {
|
||||
const api = useApi()
|
||||
const response = await api.post("/store/carts")
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
|
||||
expect(getRes.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should fail to create a cart when no region exist", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await dbConnection.manager.query(
|
||||
`UPDATE "country"
|
||||
SET region_id=null
|
||||
WHERE iso_2 = 'us'`
|
||||
)
|
||||
|
||||
await dbConnection.manager.query(`DELETE from region`)
|
||||
|
||||
try {
|
||||
await api.post("/store/carts")
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual(
|
||||
"A region is required to create a cart"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("should create a cart with items", async () => {
|
||||
const yesterday = ((today) =>
|
||||
new Date(today.setDate(today.getDate() - 1)))(new Date())
|
||||
const tomorrow = ((today) =>
|
||||
new Date(today.setDate(today.getDate() + 1)))(new Date())
|
||||
|
||||
const priceList1 = await dbConnection.manager.create(PriceList, {
|
||||
id: "pl_current",
|
||||
name: "Past winter sale",
|
||||
description: "Winter sale for key accounts.",
|
||||
type: "sale",
|
||||
status: "active",
|
||||
starts_at: yesterday,
|
||||
ends_at: tomorrow,
|
||||
})
|
||||
|
||||
await dbConnection.manager.save(priceList1)
|
||||
|
||||
const ma_sale_1 = dbConnection.manager.create(MoneyAmount, {
|
||||
variant_id: prodSale.variants[0].id,
|
||||
currency_code: "usd",
|
||||
amount: 800,
|
||||
price_list_id: "pl_current",
|
||||
})
|
||||
|
||||
await dbConnection.manager.save(ma_sale_1)
|
||||
|
||||
const api = useApi()
|
||||
|
||||
const response = await api
|
||||
.post("/store/carts", {
|
||||
items: [
|
||||
{
|
||||
variant_id: prod1.variants[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
variant_id: prodSale.variants[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
|
||||
response.data.cart.items.sort((a, b) => a.quantity - b.quantity)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.cart.items).toHaveLength(2)
|
||||
expect(response.data.cart.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
variant_id: prod1.variants[0].id,
|
||||
quantity: 1,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
variant_id: prodSale.variants[0].id,
|
||||
quantity: 2,
|
||||
unit_price: 800,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
|
||||
expect(getRes.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should create a cart with country", async () => {
|
||||
const api = useApi()
|
||||
const response = await api.post("/store/carts", {
|
||||
country_code: "us",
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.cart.shipping_address.country_code).toEqual("us")
|
||||
|
||||
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
|
||||
expect(getRes.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should create a cart with context", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const response = await api.post("/store/carts", {
|
||||
context: {
|
||||
test_id: "test",
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
const getRes = await api.post(`/store/carts/${response.data.cart.id}`)
|
||||
expect(getRes.status).toEqual(200)
|
||||
|
||||
const cart = getRes.data.cart
|
||||
expect(cart.context).toEqual({
|
||||
ip: expect.any(String),
|
||||
user_agent: expect.stringContaining("axios/0.21."),
|
||||
test_id: "test",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -35,6 +35,7 @@ module.exports = {
|
||||
featureFlags: {
|
||||
workflows: {
|
||||
[Workflows.CreateProducts]: true,
|
||||
[Workflows.CreateCart]: true,
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { MedusaContainer } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
Workflows,
|
||||
createCart as createCartWorkflow,
|
||||
} from "@medusajs/workflows"
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
@@ -7,10 +12,11 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { MedusaError, isDefined } from "medusa-core-utils"
|
||||
import reqIp from "request-ip"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { Logger } from "@medusajs/types"
|
||||
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
|
||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||
import { Cart, LineItem } from "../../../../models"
|
||||
@@ -75,18 +81,56 @@ import { FlagRouter } from "../../../../utils/flag-router"
|
||||
* $ref: "#/components/responses/500_error"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
const validated = req.validatedBody as StorePostCartReq
|
||||
const logger: Logger = req.scope.resolve("logger")
|
||||
|
||||
const reqContext = {
|
||||
ip: reqIp.getClientIp(req),
|
||||
user_agent: req.get("user-agent"),
|
||||
}
|
||||
|
||||
const isWorkflowEnabled = featureFlagRouter.isFeatureEnabled({
|
||||
workflows: Workflows.CreateCart,
|
||||
})
|
||||
|
||||
if (isWorkflowEnabled) {
|
||||
const cartWorkflow = createCartWorkflow(req.scope as MedusaContainer)
|
||||
const input = {
|
||||
...validated,
|
||||
publishableApiKeyScopes: req.publishableApiKeyScopes,
|
||||
context: {
|
||||
...reqContext,
|
||||
...validated.context,
|
||||
},
|
||||
config: {
|
||||
retrieveConfig: {
|
||||
select: defaultStoreCartFields,
|
||||
relations: defaultStoreCartRelations,
|
||||
},
|
||||
},
|
||||
}
|
||||
const { result, errors } = await cartWorkflow.run({
|
||||
input,
|
||||
context: {
|
||||
manager: entityManager,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors)) {
|
||||
if (isDefined(errors[0])) {
|
||||
throw errors[0].error
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({ cart: cleanResponseData(result, []) })
|
||||
}
|
||||
|
||||
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
|
||||
const cartService: CartService = req.scope.resolve("cartService")
|
||||
const regionService: RegionService = req.scope.resolve("regionService")
|
||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
|
||||
|
||||
let regionId!: string
|
||||
if (isDefined(validated.region_id)) {
|
||||
|
||||
15
packages/types/src/address/common.ts
Normal file
15
packages/types/src/address/common.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type AddressDTO = {
|
||||
id?: string
|
||||
address_1: string
|
||||
address_2?: string | null
|
||||
company?: string | null
|
||||
country_code: string
|
||||
city?: string | null
|
||||
phone?: string | null
|
||||
postal_code?: string | null
|
||||
province?: string | null
|
||||
metadata?: Record<string, unknown> | null
|
||||
created_at?: string | Date
|
||||
updated_at?: string | Date
|
||||
deleted_at?: string | Date | null
|
||||
}
|
||||
1
packages/types/src/address/index.ts
Normal file
1
packages/types/src/address/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./common"
|
||||
27
packages/types/src/cart/common.ts
Normal file
27
packages/types/src/cart/common.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export type CartDTO = {
|
||||
id?: string
|
||||
email?: string
|
||||
billing_address_id?: string
|
||||
shipping_address_id?: string
|
||||
region_id?: string
|
||||
customer_id?: string
|
||||
payment_id?: string
|
||||
completed_at?: Date
|
||||
payment_authorized_at?: Date
|
||||
idempotency_key?: string
|
||||
context?: Record<string, unknown>
|
||||
metadata?: Record<string, unknown>
|
||||
sales_channel_id?: string | null
|
||||
shipping_total?: number
|
||||
discount_total?: number
|
||||
raw_discount_total?: number
|
||||
item_tax_total?: number | null
|
||||
shipping_tax_total?: number | null
|
||||
tax_total?: number | null
|
||||
refunded_total?: number
|
||||
total?: number
|
||||
subtotal?: number
|
||||
refundable_amount?: number
|
||||
gift_card_total?: number
|
||||
gift_card_tax_total?: number
|
||||
}
|
||||
1
packages/types/src/cart/index.ts
Normal file
1
packages/types/src/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./common"
|
||||
@@ -1,18 +1,20 @@
|
||||
export * from "./address"
|
||||
export * from "./bundles"
|
||||
export * from "./cache"
|
||||
export * from "./cart"
|
||||
export * from "./common"
|
||||
export * from "./dal"
|
||||
export * from "./event-bus"
|
||||
export * from "./feature-flag"
|
||||
export * from "./inventory"
|
||||
export * from "./joiner"
|
||||
export * from "./logger"
|
||||
export * from "./modules-sdk"
|
||||
export * from "./product"
|
||||
export * from "./product-category"
|
||||
export * from "./sales-channel"
|
||||
export * from "./search"
|
||||
export * from "./shared-context"
|
||||
export * from "./stock-location"
|
||||
export * from "./transaction-base"
|
||||
export * from "./dal"
|
||||
export * from "./logger"
|
||||
export * from "./feature-flag"
|
||||
export * from "./sales-channel"
|
||||
export * from "./workflow"
|
||||
|
||||
22
packages/types/src/workflow/cart/create-cart.ts
Normal file
22
packages/types/src/workflow/cart/create-cart.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AddressDTO } from "../../address"
|
||||
import { WorkflowInputConfig } from "../common"
|
||||
|
||||
export interface CreateLineItemInputDTO {
|
||||
variant_id: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export interface CreateCartWorkflowInputDTO {
|
||||
config?: WorkflowInputConfig
|
||||
region_id?: string
|
||||
country_code?: string
|
||||
items?: CreateLineItemInputDTO[]
|
||||
context?: object
|
||||
sales_channel_id?: string
|
||||
shipping_address_id?: string
|
||||
billing_address_id?: string
|
||||
billing_address?: AddressDTO
|
||||
shipping_address?: AddressDTO
|
||||
customer_id?: string
|
||||
email?: string
|
||||
}
|
||||
1
packages/types/src/workflow/cart/index.ts
Normal file
1
packages/types/src/workflow/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./create-cart"
|
||||
@@ -1,2 +1,3 @@
|
||||
export * as ProductWorkflow from "./product"
|
||||
export * as CartWorkflow from "./cart"
|
||||
export * as CommonWorkflow from "./common"
|
||||
export * as ProductWorkflow from "./product"
|
||||
|
||||
@@ -88,10 +88,6 @@ export interface CreateProductInputDTO {
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
sales_channels?: CreateProductSalesChannelInputDTO[]
|
||||
listConfig: {
|
||||
select: string[]
|
||||
relations: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CreateProductsWorkflowInputDTO {
|
||||
|
||||
238
packages/workflows/src/definition/cart/create-cart.ts
Normal file
238
packages/workflows/src/definition/cart/create-cart.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import {
|
||||
TransactionStepsDefinition,
|
||||
WorkflowManager,
|
||||
} from "@medusajs/orchestration"
|
||||
import { CartWorkflow } from "@medusajs/types"
|
||||
|
||||
import { Workflows } from "../../definitions"
|
||||
import {
|
||||
AddressHandlers,
|
||||
CartHandlers,
|
||||
CommonHandlers,
|
||||
CustomerHandlers,
|
||||
RegionHandlers,
|
||||
SalesChannelHandlers,
|
||||
} from "../../handlers"
|
||||
import { aggregateData, exportWorkflow, pipe } from "../../helper"
|
||||
|
||||
enum CreateCartActions {
|
||||
setConfig = "setConfig",
|
||||
setContext = "setContext",
|
||||
attachLineItems = "attachLineItems",
|
||||
findRegion = "findRegion",
|
||||
findSalesChannel = "findSalesChannel",
|
||||
createCart = "createCart",
|
||||
findOrCreateAddresses = "findOrCreateAddresses",
|
||||
findOrCreateCustomer = "findOrCreateCustomer",
|
||||
removeCart = "removeCart",
|
||||
removeAddresses = "removeAddresses",
|
||||
retrieveCart = "retrieveCart",
|
||||
}
|
||||
|
||||
const workflowAlias = "cart"
|
||||
const getWorkflowInput = (alias = workflowAlias) => ({
|
||||
inputAlias: workflowAlias,
|
||||
invoke: {
|
||||
from: workflowAlias,
|
||||
alias,
|
||||
},
|
||||
})
|
||||
|
||||
const workflowSteps: TransactionStepsDefinition = {
|
||||
next: [
|
||||
{
|
||||
action: CreateCartActions.setConfig,
|
||||
noCompensation: true,
|
||||
},
|
||||
{
|
||||
action: CreateCartActions.findOrCreateCustomer,
|
||||
noCompensation: true,
|
||||
},
|
||||
{
|
||||
action: CreateCartActions.findSalesChannel,
|
||||
noCompensation: true,
|
||||
},
|
||||
{
|
||||
action: CreateCartActions.setContext,
|
||||
noCompensation: true,
|
||||
},
|
||||
{
|
||||
action: CreateCartActions.findRegion,
|
||||
noCompensation: true,
|
||||
next: {
|
||||
action: CreateCartActions.findOrCreateAddresses,
|
||||
noCompensation: true,
|
||||
next: {
|
||||
action: CreateCartActions.createCart,
|
||||
next: {
|
||||
action: CreateCartActions.attachLineItems,
|
||||
noCompensation: true,
|
||||
next: {
|
||||
action: CreateCartActions.retrieveCart,
|
||||
noCompensation: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const handlers = new Map([
|
||||
[
|
||||
CreateCartActions.setConfig,
|
||||
{
|
||||
invoke: pipe(
|
||||
getWorkflowInput(CommonHandlers.setConfig.aliases.Config),
|
||||
aggregateData(),
|
||||
CommonHandlers.setConfig
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.findOrCreateCustomer,
|
||||
{
|
||||
invoke: pipe(
|
||||
getWorkflowInput(
|
||||
CustomerHandlers.findOrCreateCustomer.aliases.Customer
|
||||
),
|
||||
CustomerHandlers.findOrCreateCustomer
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.findSalesChannel,
|
||||
{
|
||||
invoke: pipe(
|
||||
getWorkflowInput(
|
||||
SalesChannelHandlers.findSalesChannel.aliases.SalesChannel
|
||||
),
|
||||
SalesChannelHandlers.findSalesChannel
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.setContext,
|
||||
{
|
||||
invoke: pipe(
|
||||
getWorkflowInput(CommonHandlers.setContext.aliases.Context),
|
||||
CommonHandlers.setContext
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.findRegion,
|
||||
{
|
||||
invoke: pipe(
|
||||
getWorkflowInput(RegionHandlers.findRegion.aliases.Region),
|
||||
RegionHandlers.findRegion
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.findOrCreateAddresses,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
invoke: [
|
||||
getWorkflowInput(
|
||||
AddressHandlers.findOrCreateAddresses.aliases.Addresses
|
||||
).invoke,
|
||||
{
|
||||
from: CreateCartActions.findRegion,
|
||||
alias: AddressHandlers.findOrCreateAddresses.aliases.Region,
|
||||
},
|
||||
],
|
||||
},
|
||||
AddressHandlers.findOrCreateAddresses
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.createCart,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
invoke: [
|
||||
{
|
||||
from: CreateCartActions.findRegion,
|
||||
alias: CartHandlers.createCart.aliases.Region,
|
||||
},
|
||||
{
|
||||
from: CreateCartActions.setContext,
|
||||
alias: CartHandlers.createCart.aliases.Context,
|
||||
},
|
||||
{
|
||||
from: CreateCartActions.findOrCreateCustomer,
|
||||
alias: CartHandlers.createCart.aliases.Customer,
|
||||
},
|
||||
{
|
||||
from: CreateCartActions.findOrCreateAddresses,
|
||||
alias: CartHandlers.createCart.aliases.Addresses,
|
||||
},
|
||||
],
|
||||
},
|
||||
CartHandlers.createCart
|
||||
),
|
||||
compensate: pipe(
|
||||
{
|
||||
invoke: [
|
||||
{
|
||||
from: CreateCartActions.createCart,
|
||||
alias: CartHandlers.removeCart.aliases.Cart,
|
||||
},
|
||||
],
|
||||
},
|
||||
CartHandlers.removeCart
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.attachLineItems,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
invoke: [
|
||||
getWorkflowInput(
|
||||
CartHandlers.attachLineItemsToCart.aliases.LineItems
|
||||
).invoke,
|
||||
{
|
||||
from: CreateCartActions.createCart,
|
||||
alias: CartHandlers.attachLineItemsToCart.aliases.Cart,
|
||||
},
|
||||
],
|
||||
},
|
||||
CartHandlers.attachLineItemsToCart
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
CreateCartActions.retrieveCart,
|
||||
{
|
||||
invoke: pipe(
|
||||
{
|
||||
invoke: [
|
||||
{
|
||||
from: CreateCartActions.setConfig,
|
||||
alias: CommonHandlers.setConfig.aliases.Config,
|
||||
},
|
||||
{
|
||||
from: CreateCartActions.createCart,
|
||||
alias: CartHandlers.retrieveCart.aliases.Cart,
|
||||
},
|
||||
],
|
||||
},
|
||||
CartHandlers.retrieveCart
|
||||
),
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
WorkflowManager.register(Workflows.CreateCart, workflowSteps, handlers)
|
||||
|
||||
type CreateCartWorkflowOutput = Record<any, any>
|
||||
|
||||
export const createCart = exportWorkflow<
|
||||
CartWorkflow.CreateCartWorkflowInputDTO,
|
||||
CreateCartWorkflowOutput
|
||||
>(Workflows.CreateCart, CreateCartActions.retrieveCart)
|
||||
1
packages/workflows/src/definition/cart/index.ts
Normal file
1
packages/workflows/src/definition/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./create-cart"
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./create-products"
|
||||
export * from "./cart"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export enum Workflows {
|
||||
// Product workflows
|
||||
CreateProducts = "create-products",
|
||||
|
||||
// Cart workflows
|
||||
CreateCart = "create-cart",
|
||||
}
|
||||
|
||||
export enum InputAlias {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { AddressDTO } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type AddressesDTO = {
|
||||
shipping_address_id?: string
|
||||
billing_address_id?: string
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
addresses: AddressesDTO & {
|
||||
billing_address?: AddressDTO
|
||||
shipping_address?: AddressDTO
|
||||
}
|
||||
region: {
|
||||
region_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Addresses = "addresses",
|
||||
Region = "region",
|
||||
}
|
||||
|
||||
export async function findOrCreateAddresses({
|
||||
container,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<AddressesDTO> {
|
||||
const regionService = container.resolve("regionService")
|
||||
const addressRepository = container.resolve("addressRepository")
|
||||
|
||||
const shippingAddress = data[Aliases.Addresses].shipping_address
|
||||
const shippingAddressId = data[Aliases.Addresses].shipping_address_id
|
||||
const billingAddress = data[Aliases.Addresses].billing_address
|
||||
const billingAddressId = data[Aliases.Addresses].billing_address_id
|
||||
const addressesDTO: AddressesDTO = {}
|
||||
|
||||
const region = await regionService.retrieve(data[Aliases.Region].region_id, {
|
||||
relations: ["countries"],
|
||||
})
|
||||
|
||||
const regionCountries = region.countries.map(({ iso_2 }) => iso_2)
|
||||
|
||||
if (!shippingAddress && !shippingAddressId) {
|
||||
if (region.countries.length === 1) {
|
||||
const shippingAddress = addressRepository.create({
|
||||
country_code: regionCountries[0],
|
||||
})
|
||||
|
||||
addressesDTO.shipping_address_id = shippingAddress?.id
|
||||
}
|
||||
} else {
|
||||
if (shippingAddress) {
|
||||
if (!regionCountries.includes(shippingAddress.country_code!)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Shipping country not in region"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (shippingAddressId) {
|
||||
const address = await regionService.findOne({
|
||||
where: { id: shippingAddressId },
|
||||
})
|
||||
|
||||
if (
|
||||
address?.country_code &&
|
||||
!regionCountries.includes(address.country_code)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Shipping country not in region"
|
||||
)
|
||||
}
|
||||
|
||||
addressesDTO.shipping_address_id = address.id
|
||||
}
|
||||
}
|
||||
|
||||
if (billingAddress) {
|
||||
if (!regionCountries.includes(billingAddress.country_code!)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Billing country not in region"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (billingAddressId) {
|
||||
const address = await regionService.findOne({
|
||||
where: { id: billingAddressId },
|
||||
})
|
||||
|
||||
if (
|
||||
address?.country_code &&
|
||||
!regionCountries.includes(address.country_code)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Billing country not in region"
|
||||
)
|
||||
}
|
||||
|
||||
addressesDTO.billing_address_id = billingAddressId
|
||||
}
|
||||
|
||||
return addressesDTO
|
||||
}
|
||||
|
||||
findOrCreateAddresses.aliases = Aliases
|
||||
1
packages/workflows/src/handlers/address/index.ts
Normal file
1
packages/workflows/src/handlers/address/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./find-or-create-addresses"
|
||||
@@ -0,0 +1,57 @@
|
||||
import { CartWorkflow } from "@medusajs/types"
|
||||
import { SalesChannelFeatureFlag } from "@medusajs/utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type HandlerInputData = {
|
||||
line_items: {
|
||||
items?: CartWorkflow.CreateLineItemInputDTO[]
|
||||
}
|
||||
cart: {
|
||||
id: string
|
||||
customer_id: string
|
||||
region_id: string
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
LineItems = "line_items",
|
||||
Cart = "cart",
|
||||
}
|
||||
|
||||
export async function attachLineItemsToCart({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<void> {
|
||||
const { manager } = context
|
||||
|
||||
const featureFlagRouter = container.resolve("featureFlagRouter")
|
||||
const lineItemService = container.resolve("lineItemService")
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
const lineItemServiceTx = lineItemService.withTransaction(manager)
|
||||
const cartServiceTx = cartService.withTransaction(manager)
|
||||
let lineItems = data[Aliases.LineItems].items
|
||||
const cart = data[Aliases.Cart]
|
||||
|
||||
if (lineItems?.length) {
|
||||
const generateInputData = lineItems.map((item) => ({
|
||||
variantId: item.variant_id,
|
||||
quantity: item.quantity,
|
||||
}))
|
||||
|
||||
lineItems = await lineItemServiceTx.generate(generateInputData, {
|
||||
region_id: cart.region_id,
|
||||
customer_id: cart.customer_id,
|
||||
})
|
||||
|
||||
await cartServiceTx.addOrUpdateLineItems(cart.id, lineItems, {
|
||||
validateSalesChannels: featureFlagRouter.isFeatureEnabled(
|
||||
SalesChannelFeatureFlag.key
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
attachLineItemsToCart.aliases = Aliases
|
||||
57
packages/workflows/src/handlers/cart/create-cart.ts
Normal file
57
packages/workflows/src/handlers/cart/create-cart.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { CartDTO } from "@medusajs/types"
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
enum Aliases {
|
||||
SalesChannel = "SalesChannel",
|
||||
Addresses = "addresses",
|
||||
Customer = "customer",
|
||||
Region = "region",
|
||||
Context = "context",
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
sales_channel: {
|
||||
sales_channel_id?: string
|
||||
}
|
||||
addresses: {
|
||||
shipping_address_id: string
|
||||
billing_address_id: string
|
||||
}
|
||||
customer: {
|
||||
customer_id?: string
|
||||
email?: string
|
||||
}
|
||||
region: {
|
||||
region_id: string
|
||||
}
|
||||
context: {
|
||||
context: Record<any, any>
|
||||
}
|
||||
}
|
||||
|
||||
type HandlerOutputData = {
|
||||
cart: CartDTO
|
||||
}
|
||||
|
||||
export async function createCart({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<HandlerOutputData> {
|
||||
const { manager } = context
|
||||
|
||||
const cartService = container.resolve("cartService")
|
||||
const cartServiceTx = cartService.withTransaction(manager)
|
||||
|
||||
const cart = await cartServiceTx.create({
|
||||
...data[Aliases.SalesChannel],
|
||||
...data[Aliases.Addresses],
|
||||
...data[Aliases.Customer],
|
||||
...data[Aliases.Region],
|
||||
...data[Aliases.Context],
|
||||
})
|
||||
|
||||
return cart
|
||||
}
|
||||
|
||||
createCart.aliases = Aliases
|
||||
4
packages/workflows/src/handlers/cart/index.ts
Normal file
4
packages/workflows/src/handlers/cart/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./attach-line-items-to-cart"
|
||||
export * from "./create-cart"
|
||||
export * from "./remove-cart"
|
||||
export * from "./retrieve-cart"
|
||||
28
packages/workflows/src/handlers/cart/remove-cart.ts
Normal file
28
packages/workflows/src/handlers/cart/remove-cart.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
enum Aliases {
|
||||
Cart = "cart",
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
cart: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeCart({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<void> {
|
||||
const { manager } = context
|
||||
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
const cartServiceTx = cartService.withTransaction(manager)
|
||||
const cart = data[Aliases.Cart]
|
||||
|
||||
await cartServiceTx.delete(cart.id)
|
||||
}
|
||||
|
||||
removeCart.aliases = Aliases
|
||||
40
packages/workflows/src/handlers/cart/retrieve-cart.ts
Normal file
40
packages/workflows/src/handlers/cart/retrieve-cart.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CartDTO } from "@medusajs/types"
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type HandlerInputData = {
|
||||
cart: {
|
||||
id: string
|
||||
}
|
||||
config: {
|
||||
retrieveConfig: {
|
||||
select: string[]
|
||||
relations: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Cart = "cart",
|
||||
Config = "config",
|
||||
}
|
||||
|
||||
export async function retrieveCart({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<CartDTO> {
|
||||
const { manager } = context
|
||||
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
const cartServiceTx = cartService.withTransaction(manager)
|
||||
|
||||
const retrieved = await cartServiceTx.retrieve(
|
||||
data[Aliases.Cart].id,
|
||||
data[Aliases.Config].retrieveConfig
|
||||
)
|
||||
|
||||
return retrieved
|
||||
}
|
||||
|
||||
retrieveCart.aliases = Aliases
|
||||
2
packages/workflows/src/handlers/common/index.ts
Normal file
2
packages/workflows/src/handlers/common/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./set-config"
|
||||
export * from "./set-context"
|
||||
31
packages/workflows/src/handlers/common/set-config.ts
Normal file
31
packages/workflows/src/handlers/common/set-config.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type ConfigDTO = {
|
||||
retrieveConfig?: {
|
||||
select?: string[]
|
||||
relations?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Config = "config",
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
config: {
|
||||
retrieveConfig?: {
|
||||
select?: string[]
|
||||
relations?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function setConfig({
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<ConfigDTO> {
|
||||
return {
|
||||
retrieveConfig: data[Aliases.Config].retrieveConfig,
|
||||
}
|
||||
}
|
||||
|
||||
setConfig.aliases = Aliases
|
||||
27
packages/workflows/src/handlers/common/set-context.ts
Normal file
27
packages/workflows/src/handlers/common/set-context.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type ContextDTO = {
|
||||
context?: Record<any, any>
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Context = "context",
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
context: {
|
||||
context?: Record<any, any>
|
||||
}
|
||||
}
|
||||
|
||||
export async function setContext({
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<ContextDTO> {
|
||||
const contextDTO: ContextDTO = {
|
||||
context: data[Aliases.Context].context,
|
||||
}
|
||||
|
||||
return contextDTO
|
||||
}
|
||||
|
||||
setContext.aliases = Aliases
|
||||
@@ -0,0 +1,63 @@
|
||||
import { validateEmail } from "@medusajs/utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type CustomerDTO = {
|
||||
customer_id?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
customer: {
|
||||
customer_id?: string
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Customer = "customer",
|
||||
}
|
||||
|
||||
export async function findOrCreateCustomer({
|
||||
container,
|
||||
context,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<CustomerDTO> {
|
||||
const { manager } = context
|
||||
|
||||
const customerService = container.resolve("customerService")
|
||||
|
||||
const customerDTO: CustomerDTO = {}
|
||||
const customerId = data[Aliases.Customer].customer_id
|
||||
const customerServiceTx = customerService.withTransaction(manager)
|
||||
|
||||
if (customerId) {
|
||||
const customer = await customerServiceTx
|
||||
.retrieve(customerId)
|
||||
.catch(() => undefined)
|
||||
|
||||
customerDTO.customer_id = customer?.id
|
||||
customerDTO.email = customer?.email
|
||||
}
|
||||
|
||||
const customerEmail = data[Aliases.Customer].email
|
||||
|
||||
if (customerEmail) {
|
||||
const validatedEmail = validateEmail(customerEmail)
|
||||
|
||||
let customer = await customerServiceTx
|
||||
.retrieveUnregisteredByEmail(validatedEmail)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!customer) {
|
||||
customer = await customerServiceTx.create({ email: validatedEmail })
|
||||
}
|
||||
|
||||
customerDTO.customer_id = customer.id
|
||||
customerDTO.email = customer.email
|
||||
}
|
||||
|
||||
return customerDTO
|
||||
}
|
||||
|
||||
findOrCreateCustomer.aliases = Aliases
|
||||
1
packages/workflows/src/handlers/customer/index.ts
Normal file
1
packages/workflows/src/handlers/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./find-or-create-customer"
|
||||
@@ -1,3 +1,9 @@
|
||||
export * as ProductHandlers from "./product"
|
||||
export * as AddressHandlers from "./address"
|
||||
export * as CartHandlers from "./cart"
|
||||
export * as CommonHandlers from "./common"
|
||||
export * as CustomerHandlers from "./customer"
|
||||
export * as InventoryHandlers from "./inventory"
|
||||
export * as MiddlewaresHandlers from "./middlewares"
|
||||
export * as ProductHandlers from "./product"
|
||||
export * as RegionHandlers from "./region"
|
||||
export * as SalesChannelHandlers from "./sales-channel"
|
||||
|
||||
49
packages/workflows/src/handlers/region/find-region.ts
Normal file
49
packages/workflows/src/handlers/region/find-region.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { isDefined } from "medusa-core-utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type RegionDTO = {
|
||||
region_id?: string
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
region: {
|
||||
region_id: string
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
Region = "region",
|
||||
}
|
||||
|
||||
export async function findRegion({
|
||||
container,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<RegionDTO> {
|
||||
const regionService = container.resolve("regionService")
|
||||
|
||||
let regionId: string
|
||||
const regionDTO: RegionDTO = {}
|
||||
|
||||
if (isDefined(data[Aliases.Region].region_id)) {
|
||||
regionId = data[Aliases.Region].region_id
|
||||
} else {
|
||||
const regions = await regionService.list({}, {})
|
||||
|
||||
if (!regions?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`A region is required to create a cart`
|
||||
)
|
||||
}
|
||||
|
||||
regionId = regions[0].id
|
||||
}
|
||||
|
||||
regionDTO.region_id = regionId
|
||||
|
||||
return regionDTO
|
||||
}
|
||||
|
||||
findRegion.aliases = Aliases
|
||||
1
packages/workflows/src/handlers/region/index.ts
Normal file
1
packages/workflows/src/handlers/region/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./find-region"
|
||||
@@ -0,0 +1,74 @@
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { isDefined } from "medusa-core-utils"
|
||||
|
||||
import { WorkflowArguments } from "../../helper"
|
||||
|
||||
type AttachSalesChannelDTO = {
|
||||
sales_channel_id?: string
|
||||
}
|
||||
|
||||
type HandlerInputData = {
|
||||
sales_channel: {
|
||||
sales_channel_id?: string
|
||||
publishableApiKeyScopes?: {
|
||||
sales_channel_ids?: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Aliases {
|
||||
SalesChannel = "sales_channel",
|
||||
}
|
||||
|
||||
export async function findSalesChannel({
|
||||
container,
|
||||
data,
|
||||
}: WorkflowArguments<HandlerInputData>): Promise<AttachSalesChannelDTO> {
|
||||
const salesChannelService = container.resolve("salesChannelService")
|
||||
const storeService = container.resolve("storeService")
|
||||
|
||||
let salesChannelId = data[Aliases.SalesChannel].sales_channel_id
|
||||
let salesChannel
|
||||
const salesChannelDTO: AttachSalesChannelDTO = {}
|
||||
const publishableApiKeyScopes =
|
||||
data[Aliases.SalesChannel].publishableApiKeyScopes || {}
|
||||
|
||||
delete data[Aliases.SalesChannel].publishableApiKeyScopes
|
||||
|
||||
if (
|
||||
!isDefined(salesChannelId) &&
|
||||
publishableApiKeyScopes?.sales_channel_ids?.length
|
||||
) {
|
||||
if (publishableApiKeyScopes.sales_channel_ids.length > 1) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"The provided PublishableApiKey has multiple associated sales channels."
|
||||
)
|
||||
}
|
||||
|
||||
salesChannelId = publishableApiKeyScopes.sales_channel_ids[0]
|
||||
}
|
||||
|
||||
if (isDefined(salesChannelId)) {
|
||||
salesChannel = await salesChannelService.retrieve(salesChannelId)
|
||||
} else {
|
||||
salesChannel = (
|
||||
await storeService.retrieve({
|
||||
relations: ["default_sales_channel"],
|
||||
})
|
||||
).default_sales_channel
|
||||
}
|
||||
|
||||
if (salesChannel.is_disabled) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Unable to assign the cart to a disabled Sales Channel "${salesChannel.name}"`
|
||||
)
|
||||
}
|
||||
|
||||
salesChannelDTO.sales_channel_id = salesChannel?.id
|
||||
|
||||
return salesChannelDTO
|
||||
}
|
||||
|
||||
findSalesChannel.aliases = Aliases
|
||||
1
packages/workflows/src/handlers/sales-channel/index.ts
Normal file
1
packages/workflows/src/handlers/sales-channel/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./find-sales-channel"
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./aggregate"
|
||||
export * from "./empty-handler"
|
||||
export * from "./pipe"
|
||||
export * from "./workflow-export"
|
||||
|
||||
Reference in New Issue
Block a user