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:
@@ -0,0 +1,412 @@
|
||||
import {
|
||||
addToCartWorkflow,
|
||||
createCartWorkflow,
|
||||
findOrCreateCustomerStepId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ICartModuleService,
|
||||
ICustomerModuleService,
|
||||
IPricingModuleService,
|
||||
IProductModuleService,
|
||||
IRegionModuleService,
|
||||
ISalesChannelModuleService,
|
||||
} from "@medusajs/types"
|
||||
import path from "path"
|
||||
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||
import { useApi } from "../../../../environment-helpers/use-api"
|
||||
import { getContainer } from "../../../../environment-helpers/use-container"
|
||||
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||
import adminSeeder from "../../../../helpers/admin-seeder"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||
|
||||
describe("Carts workflows", () => {
|
||||
let dbConnection
|
||||
let appContainer
|
||||
let shutdownServer
|
||||
let cartModuleService: ICartModuleService
|
||||
let regionModuleService: IRegionModuleService
|
||||
let scModuleService: ISalesChannelModuleService
|
||||
let customerModule: ICustomerModuleService
|
||||
let productModule: IProductModuleService
|
||||
let pricingModule: IPricingModuleService
|
||||
let remoteLink
|
||||
|
||||
let defaultRegion
|
||||
|
||||
beforeAll(async () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||
dbConnection = await initDb({ cwd, env } as any)
|
||||
shutdownServer = await startBootstrapApp({ cwd, env })
|
||||
appContainer = getContainer()
|
||||
cartModuleService = appContainer.resolve(ModuleRegistrationName.CART)
|
||||
regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||
scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL)
|
||||
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
|
||||
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
|
||||
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
|
||||
remoteLink = appContainer.resolve("remoteLink")
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const db = useDb()
|
||||
await db.shutdown()
|
||||
await shutdownServer()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
await regionModuleService.createDefaultCountriesAndCurrencies()
|
||||
|
||||
// Here, so we don't have to create a region for each test
|
||||
defaultRegion = await regionModuleService.create({
|
||||
name: "Default Region",
|
||||
currency_code: "dkk",
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const db = useDb()
|
||||
await db.teardown()
|
||||
})
|
||||
|
||||
describe("CreateCartWorkflow", () => {
|
||||
it("should create a cart", async () => {
|
||||
const region = await regionModuleService.create({
|
||||
name: "US",
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const salesChannel = await scModuleService.create({
|
||||
name: "Webshop",
|
||||
})
|
||||
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const priceSet = await pricingModule.create({
|
||||
prices: [
|
||||
{
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
pricingService: {
|
||||
price_set_id: priceSet.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { result } = await createCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
email: "tony@stark.com",
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
sales_channel_id: salesChannel.id,
|
||||
items: [
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const cart = await cartModuleService.retrieve(result.id, {
|
||||
relations: ["items"],
|
||||
})
|
||||
|
||||
expect(cart).toEqual(
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
email: "tony@stark.com",
|
||||
region_id: region.id,
|
||||
sales_channel_id: salesChannel.id,
|
||||
customer_id: expect.any(String),
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
quantity: 1,
|
||||
unit_price: 3000,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw when no regions exist", async () => {
|
||||
await regionModuleService.delete(defaultRegion.id)
|
||||
|
||||
const { errors } = await createCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
email: "tony@stark.com",
|
||||
currency_code: "usd",
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "find-one-or-any-region",
|
||||
handlerType: "invoke",
|
||||
error: new Error("No regions found"),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should throw if sales channel is disabled", async () => {
|
||||
const api = useApi() as any
|
||||
|
||||
const salesChannel = await scModuleService.create({
|
||||
name: "Webshop",
|
||||
is_disabled: true,
|
||||
})
|
||||
|
||||
const { errors } = await createCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
sales_channel_id: salesChannel.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "find-sales-channel",
|
||||
handlerType: "invoke",
|
||||
error: new Error(
|
||||
`Unable to assign cart to disabled Sales Channel: Webshop`
|
||||
),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
describe("compensation", () => {
|
||||
it("should delete created customer if cart-creation fails", async () => {
|
||||
expect.assertions(2)
|
||||
const workflow = createCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction("throw", findOrCreateCustomerStepId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Failed to create cart`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
currency_code: "usd",
|
||||
email: "tony@stark-industries.com",
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(`Failed to create cart`),
|
||||
},
|
||||
])
|
||||
|
||||
const customers = await customerModule.list({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
expect(customers).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not delete existing customer if cart-creation fails", async () => {
|
||||
expect.assertions(2)
|
||||
const workflow = createCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction("throw", findOrCreateCustomerStepId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Failed to create cart`)
|
||||
},
|
||||
})
|
||||
|
||||
const customer = await customerModule.create({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
currency_code: "usd",
|
||||
customer_id: customer.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(`Failed to create cart`),
|
||||
},
|
||||
])
|
||||
|
||||
const customers = await customerModule.list({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
expect(customers).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("AddToCartWorkflow", () => {
|
||||
it("should add item to cart", async () => {
|
||||
let cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const priceSet = await pricingModule.create({
|
||||
prices: [
|
||||
{
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
pricingService: {
|
||||
price_set_id: priceSet.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
cart = await cartModuleService.retrieve(cart.id, {
|
||||
select: ["id", "region_id", "currency_code"],
|
||||
})
|
||||
|
||||
await addToCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
items: [
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
cart,
|
||||
},
|
||||
})
|
||||
|
||||
cart = await cartModuleService.retrieve(cart.id, {
|
||||
relations: ["items"],
|
||||
})
|
||||
|
||||
expect(cart).toEqual(
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
currency_code: "usd",
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
unit_price: 3000,
|
||||
quantity: 1,
|
||||
title: "Test variant",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw if no price sets for variant exist", async () => {
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const { errors } = await addToCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
items: [
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
cart,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "get-variant-price-sets",
|
||||
handlerType: "invoke",
|
||||
error: new Error(
|
||||
`Variants with IDs ${product.variants[0].id} do not have a price`
|
||||
),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should throw if variant does not exist", async () => {
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const { errors } = await addToCartWorkflow(appContainer).run({
|
||||
input: {
|
||||
items: [
|
||||
{
|
||||
variant_id: "prva_foo",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
cart,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "validate-variants-exist",
|
||||
handlerType: "invoke",
|
||||
error: new Error(`Variants with IDs prva_foo do not exist`),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
createCartWorkflow,
|
||||
findOrCreateCustomerStepId,
|
||||
} from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ICartModuleService,
|
||||
ICustomerModuleService,
|
||||
IPricingModuleService,
|
||||
IProductModuleService,
|
||||
IRegionModuleService,
|
||||
ISalesChannelModuleService,
|
||||
} from "@medusajs/types"
|
||||
@@ -29,6 +27,9 @@ describe("Store Carts API", () => {
|
||||
let regionModuleService: IRegionModuleService
|
||||
let scModuleService: ISalesChannelModuleService
|
||||
let customerModule: ICustomerModuleService
|
||||
let productModule: IProductModuleService
|
||||
let pricingModule: IPricingModuleService
|
||||
let remoteLink
|
||||
|
||||
let defaultRegion
|
||||
|
||||
@@ -41,6 +42,9 @@ describe("Store Carts API", () => {
|
||||
regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||
scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL)
|
||||
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
|
||||
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
|
||||
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
|
||||
remoteLink = appContainer.resolve("remoteLink")
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -51,7 +55,6 @@ describe("Store Carts API", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await adminSeeder(dbConnection)
|
||||
// @ts-ignore
|
||||
await regionModuleService.createDefaultCountriesAndCurrencies()
|
||||
|
||||
// Here, so we don't have to create a region for each test
|
||||
@@ -77,6 +80,58 @@ describe("Store Carts API", () => {
|
||||
name: "Webshop",
|
||||
})
|
||||
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
{
|
||||
title: "Test variant 2",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const [priceSet, priceSetTwo] = await pricingModule.create([
|
||||
{
|
||||
prices: [
|
||||
{
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
prices: [
|
||||
{
|
||||
amount: 4000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
pricingService: {
|
||||
price_set_id: priceSet.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
productService: {
|
||||
variant_id: product.variants[1].id,
|
||||
},
|
||||
pricingService: {
|
||||
price_set_id: priceSetTwo.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const api = useApi() as any
|
||||
|
||||
const created = await api.post(`/store/carts`, {
|
||||
@@ -84,6 +139,16 @@ describe("Store Carts API", () => {
|
||||
currency_code: "usd",
|
||||
region_id: region.id,
|
||||
sales_channel_id: salesChannel.id,
|
||||
items: [
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
variant_id: product.variants[1].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(created.status).toEqual(200)
|
||||
@@ -100,6 +165,16 @@ describe("Store Carts API", () => {
|
||||
customer: expect.objectContaining({
|
||||
email: "tony@stark.com",
|
||||
}),
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
quantity: 1,
|
||||
unit_price: 3000,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
quantity: 2,
|
||||
unit_price: 4000,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -202,19 +277,6 @@ describe("Store Carts API", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw when no regions exist", async () => {
|
||||
const api = useApi() as any
|
||||
|
||||
await regionModuleService.delete(defaultRegion.id)
|
||||
|
||||
await expect(
|
||||
api.post(`/store/carts`, {
|
||||
email: "tony@stark.com",
|
||||
currency_code: "usd",
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should respond 400 bad request on unknown props", async () => {
|
||||
const api = useApi() as any
|
||||
|
||||
@@ -224,93 +286,6 @@ describe("Store Carts API", () => {
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it("should throw if sales channel is disabled", async () => {
|
||||
const api = useApi() as any
|
||||
|
||||
const salesChannel = await scModuleService.create({
|
||||
name: "Webshop",
|
||||
is_disabled: true,
|
||||
})
|
||||
|
||||
await expect(
|
||||
api.post(`/store/carts`, {
|
||||
sales_channel_id: salesChannel.id,
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
describe("compensation", () => {
|
||||
it("should delete created customer if cart-creation fails", async () => {
|
||||
expect.assertions(2)
|
||||
const workflow = createCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction("throw", findOrCreateCustomerStepId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Failed to create cart`)
|
||||
},
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
currency_code: "usd",
|
||||
email: "tony@stark-industries.com",
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(`Failed to create cart`),
|
||||
},
|
||||
])
|
||||
|
||||
const customers = await customerModule.list({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
expect(customers).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not delete existing customer if cart-creation fails", async () => {
|
||||
expect.assertions(2)
|
||||
const workflow = createCartWorkflow(appContainer)
|
||||
|
||||
workflow.appendAction("throw", findOrCreateCustomerStepId, {
|
||||
invoke: async function failStep() {
|
||||
throw new Error(`Failed to create cart`)
|
||||
},
|
||||
})
|
||||
|
||||
const customer = await customerModule.create({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
currency_code: "usd",
|
||||
customer_id: customer.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "throw",
|
||||
handlerType: "invoke",
|
||||
error: new Error(`Failed to create cart`),
|
||||
},
|
||||
])
|
||||
|
||||
const customers = await customerModule.list({
|
||||
email: "tony@stark-industries.com",
|
||||
})
|
||||
|
||||
expect(customers).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("GET /store/carts/:id", () => {
|
||||
@@ -410,4 +385,64 @@ describe("Store Carts API", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /store/carts/:id/line-items", () => {
|
||||
it("should add item to cart", async () => {
|
||||
const cart = await cartModuleService.create({
|
||||
currency_code: "usd",
|
||||
})
|
||||
|
||||
const [product] = await productModule.create([
|
||||
{
|
||||
title: "Test product",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const priceSet = await pricingModule.create({
|
||||
prices: [
|
||||
{
|
||||
amount: 3000,
|
||||
currency_code: "usd",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
pricingService: {
|
||||
price_set_id: priceSet.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const api = useApi() as any
|
||||
const response = await api.post(`/store/carts/${cart.id}/line-items`, {
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
})
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.cart).toEqual(
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
currency_code: "usd",
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
unit_price: 3000,
|
||||
quantity: 1,
|
||||
title: "Test variant",
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,10 +2,10 @@ import { useApi } from "../../../environment-helpers/use-api"
|
||||
import { initDb, useDb } from "../../../environment-helpers/use-db"
|
||||
|
||||
import {
|
||||
StepResponse,
|
||||
WorkflowData,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
StepResponse,
|
||||
WorkflowData,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { AxiosInstance } from "axios"
|
||||
import path from "path"
|
||||
@@ -200,9 +200,9 @@ export const workflowEngineTestSuite = (env, extraParams = {}) => {
|
||||
data: expect.objectContaining({
|
||||
invoke: {
|
||||
"my-step": {
|
||||
__type: "WorkflowWorkflowData",
|
||||
__type: "Symbol(WorkflowWorkflowData)",
|
||||
output: {
|
||||
__type: "WorkflowStepResponse",
|
||||
__type: "Symbol(WorkflowStepResponse)",
|
||||
output: {
|
||||
result: "abc",
|
||||
},
|
||||
@@ -248,7 +248,7 @@ export const workflowEngineTestSuite = (env, extraParams = {}) => {
|
||||
data: expect.objectContaining({
|
||||
invoke: expect.objectContaining({
|
||||
"my-step-async": {
|
||||
__type: "WorkflowStepResponse",
|
||||
__type: "Symbol(WorkflowStepResponse)",
|
||||
output: {
|
||||
all: "good",
|
||||
},
|
||||
|
||||
31
packages/core-flows/src/definition/cart/steps/add-to-cart.ts
Normal file
31
packages/core-flows/src/definition/cart/steps/add-to-cart.ts
Normal file
@@ -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"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { addToCartWorkflow } from "@medusajs/core-flows"
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { ICartModuleService } from "@medusajs/types"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
import { defaultStoreCartFields } from "../../query-config"
|
||||
import { StorePostCartsCartLineItemsReq } from "./validators"
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const cartModuleService = req.scope.resolve<ICartModuleService>(
|
||||
ModuleRegistrationName.CART
|
||||
)
|
||||
|
||||
const cart = await cartModuleService.retrieve(req.params.id, {
|
||||
select: ["id", "region_id", "currency_code"],
|
||||
})
|
||||
|
||||
const workflowInput = {
|
||||
items: [req.validatedBody as StorePostCartsCartLineItemsReq],
|
||||
cart,
|
||||
}
|
||||
|
||||
const { errors } = await addToCartWorkflow(req.scope).run({
|
||||
input: workflowInput,
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const remoteQuery = req.scope.resolve("remoteQuery")
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: "cart",
|
||||
fields: defaultStoreCartFields,
|
||||
})
|
||||
|
||||
const [updatedCart] = await remoteQuery(query, {
|
||||
cart: { id: req.params.id },
|
||||
})
|
||||
|
||||
res.status(200).json({ cart: updatedCart })
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsInt, IsOptional, IsString } from "class-validator"
|
||||
|
||||
export class StorePostCartsCartLineItemsReq {
|
||||
@IsString()
|
||||
variant_id: string
|
||||
|
||||
@IsInt()
|
||||
quantity: number
|
||||
|
||||
@IsOptional()
|
||||
metadata?: Record<string, unknown> | undefined
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import { StorePostCartsCartLineItemsReq } from "./[id]/line-items/validators"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import {
|
||||
StoreDeleteCartsCartPromotionsReq,
|
||||
@@ -40,6 +41,11 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
matcher: "/store/carts/:id",
|
||||
middlewares: [transformBody(StorePostCartsCartReq)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/store/carts/:id/line-items",
|
||||
middlewares: [transformBody(StorePostCartsCartLineItemsReq)],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/store/carts/:id/promotions",
|
||||
|
||||
@@ -3,13 +3,9 @@ import { CreateCartWorkflowInputDTO } from "@medusajs/types"
|
||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||
import { defaultStoreCartFields } from "../carts/query-config"
|
||||
import { StorePostCartReq } from "./validators"
|
||||
|
||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||
const input = req.validatedBody as StorePostCartReq
|
||||
const workflowInput: CreateCartWorkflowInputDTO = {
|
||||
...input,
|
||||
}
|
||||
const workflowInput = req.validatedBody as CreateCartWorkflowInputDTO
|
||||
|
||||
// If the customer is logged in, we auto-assign them to the cart
|
||||
if (req.auth_user?.app_metadata?.customer_id) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextFunction, Request, Response } from "express"
|
||||
|
||||
import { Logger } from "../../types/global"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { Logger } from "../../types/global"
|
||||
import { formatException } from "../../utils"
|
||||
|
||||
const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { handlePostgresDatabaseError } from "@medusajs/utils"
|
||||
import { AwilixContainer } from "awilix"
|
||||
import {
|
||||
DataSource,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
} from "typeorm"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import "../utils/naming-strategy"
|
||||
import { handlePostgresDatabaseError } from "@medusajs/utils"
|
||||
|
||||
type Options = {
|
||||
configModule: ConfigModule
|
||||
|
||||
@@ -4,18 +4,18 @@ import {
|
||||
PriceSetMoneyAmountDTO,
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
CustomerService,
|
||||
ProductVariantService,
|
||||
RegionService,
|
||||
TaxProviderService,
|
||||
} from "."
|
||||
import {
|
||||
FlagRouter,
|
||||
MedusaV2Flag,
|
||||
promiseAll,
|
||||
removeNullish,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
CustomerService,
|
||||
ProductVariantService,
|
||||
RegionService,
|
||||
TaxProviderService,
|
||||
} from "."
|
||||
import {
|
||||
IPriceSelectionStrategy,
|
||||
PriceSelectionContext,
|
||||
@@ -36,11 +36,11 @@ import {
|
||||
TaxedPricing,
|
||||
} from "../types/pricing"
|
||||
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { calculatePriceTaxAmount } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
export interface CreateCartLineItemDTO {
|
||||
variant_id: string
|
||||
import { CartDTO } from "./common"
|
||||
|
||||
export interface CreateCartCreateLineItemDTO {
|
||||
quantity: number
|
||||
variant_id: string
|
||||
title?: string
|
||||
|
||||
subtitle?: string
|
||||
thumbnail?: string
|
||||
|
||||
product_id?: string
|
||||
product_title?: string
|
||||
product_description?: string
|
||||
product_subtitle?: string
|
||||
product_type?: string
|
||||
product_collection?: string
|
||||
product_handle?: string
|
||||
|
||||
variant_sku?: string
|
||||
variant_barcode?: string
|
||||
variant_title?: string
|
||||
variant_option_values?: Record<string, unknown>
|
||||
|
||||
requires_shipping?: boolean
|
||||
is_discountable?: boolean
|
||||
is_tax_inclusive?: boolean
|
||||
is_giftcard?: boolean
|
||||
|
||||
compare_at_unit_price?: number
|
||||
unit_price?: number | string
|
||||
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface CreateCartAddressDTO {
|
||||
@@ -29,5 +58,10 @@ export interface CreateCartWorkflowInputDTO {
|
||||
billing_address?: CreateCartAddressDTO | string
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
items?: CreateCartLineItemDTO[]
|
||||
items?: CreateCartCreateLineItemDTO[]
|
||||
}
|
||||
|
||||
export interface AddToCartWorkflowInputDTO {
|
||||
items: CreateCartCreateLineItemDTO[]
|
||||
cart: CartDTO
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export interface ProductDTO {
|
||||
*
|
||||
* @expandable
|
||||
*/
|
||||
type: ProductTypeDTO[]
|
||||
type: ProductTypeDTO
|
||||
/**
|
||||
* The associated product tags.
|
||||
*
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
export const SymbolMedusaWorkflowComposerContext = Symbol.for(
|
||||
"MedusaWorkflowComposerContext"
|
||||
)
|
||||
export const SymbolInputReference = Symbol.for("WorkflowInputReference")
|
||||
export const SymbolWorkflowStep = Symbol.for("WorkflowStep")
|
||||
export const SymbolWorkflowHook = Symbol.for("WorkflowHook")
|
||||
export const SymbolWorkflowWorkflowData = Symbol.for("WorkflowWorkflowData")
|
||||
export const SymbolWorkflowStepResponse = Symbol.for("WorkflowStepResponse")
|
||||
export const SymbolWorkflowStepBind = Symbol.for("WorkflowStepBind")
|
||||
).toString()
|
||||
export const SymbolInputReference = Symbol.for(
|
||||
"WorkflowInputReference"
|
||||
).toString()
|
||||
export const SymbolWorkflowStep = Symbol.for("WorkflowStep").toString()
|
||||
export const SymbolWorkflowHook = Symbol.for("WorkflowHook").toString()
|
||||
export const SymbolWorkflowWorkflowData = Symbol.for(
|
||||
"WorkflowWorkflowData"
|
||||
).toString()
|
||||
export const SymbolWorkflowStepResponse = Symbol.for(
|
||||
"WorkflowStepResponse"
|
||||
).toString()
|
||||
export const SymbolWorkflowStepBind = Symbol.for("WorkflowStepBind").toString()
|
||||
export const SymbolWorkflowStepTransformer = Symbol.for(
|
||||
"WorkflowStepTransformer"
|
||||
)
|
||||
).toString()
|
||||
|
||||
@@ -55,7 +55,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
|
||||
])
|
||||
}
|
||||
|
||||
private stringifyWithSymbol(key, value) {
|
||||
/*private stringifyWithSymbol(key, value) {
|
||||
if (key === "__type" && typeof value === "symbol") {
|
||||
return Symbol.keyFor(value)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}*/
|
||||
|
||||
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
||||
return this.storage.get(key)
|
||||
@@ -105,7 +105,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
|
||||
})
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify(data, this.stringifyWithSymbol)
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
const parsedData = JSON.parse(stringifiedData)
|
||||
|
||||
if (hasFinished && !retentionTime) {
|
||||
|
||||
@@ -99,7 +99,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
||||
})
|
||||
}
|
||||
|
||||
private stringifyWithSymbol(key, value) {
|
||||
/*private stringifyWithSymbol(key, value) {
|
||||
if (key === "__type" && typeof value === "symbol") {
|
||||
return Symbol.keyFor(value)
|
||||
}
|
||||
@@ -113,12 +113,12 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}*/
|
||||
|
||||
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
||||
const data = await this.redisClient.get(key)
|
||||
|
||||
return data ? JSON.parse(data, this.jsonWithSymbol) : undefined
|
||||
return data ? JSON.parse(data) : undefined
|
||||
}
|
||||
|
||||
async list(): Promise<TransactionCheckpoint[]> {
|
||||
@@ -129,7 +129,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
||||
for (const key of keys) {
|
||||
const data = await this.redisClient.get(key)
|
||||
if (data) {
|
||||
transactions.push(JSON.parse(data, this.jsonWithSymbol))
|
||||
transactions.push(JSON.parse(data))
|
||||
}
|
||||
}
|
||||
return transactions
|
||||
@@ -159,7 +159,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
||||
})
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify(data, this.stringifyWithSymbol)
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
const parsedData = JSON.parse(stringifiedData)
|
||||
|
||||
if (!hasFinished) {
|
||||
|
||||
@@ -10,6 +10,15 @@ import { Context, MedusaContainer } from "@medusajs/types"
|
||||
export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
|
||||
(this: CreateWorkflowComposerContext) => WorkflowData<TOutput>
|
||||
|
||||
type StepFunctionReturnConfig<TOutput> = {
|
||||
config(
|
||||
config: { name?: string } & Omit<
|
||||
TransactionStepsDefinition,
|
||||
"next" | "uuid" | "action"
|
||||
>
|
||||
): WorkflowData<TOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* A step function to be used in a workflow.
|
||||
*
|
||||
@@ -19,16 +28,17 @@ export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
|
||||
export type StepFunction<TInput, TOutput = unknown> = (keyof TInput extends []
|
||||
? // Function that doesn't expect any input
|
||||
{
|
||||
(): WorkflowData<TOutput>
|
||||
(): WorkflowData<TOutput> & StepFunctionReturnConfig<TOutput>
|
||||
}
|
||||
: // function that expects an input object
|
||||
{
|
||||
(input: WorkflowData<TInput> | TInput): WorkflowData<TOutput>
|
||||
(input: WorkflowData<TInput> | TInput): WorkflowData<TOutput> &
|
||||
StepFunctionReturnConfig<TOutput>
|
||||
}) &
|
||||
WorkflowDataProperties<TOutput>
|
||||
|
||||
export type WorkflowDataProperties<T = unknown> = {
|
||||
__type: Symbol
|
||||
__type: string
|
||||
__step__: string
|
||||
}
|
||||
|
||||
@@ -37,9 +47,11 @@ export type WorkflowDataProperties<T = unknown> = {
|
||||
*
|
||||
* @typeParam T - The type of a step's input or result.
|
||||
*/
|
||||
export type WorkflowData<T = unknown> = (T extends object
|
||||
export type WorkflowData<T = unknown> = (T extends Array<infer Item>
|
||||
? Array<Item | WorkflowData<Item>>
|
||||
: T extends object
|
||||
? {
|
||||
[Key in keyof T]: WorkflowData<T[Key]>
|
||||
[Key in keyof T]: T[Key] | WorkflowData<T[Key]>
|
||||
}
|
||||
: T & WorkflowDataProperties<T>) &
|
||||
T &
|
||||
|
||||
Reference in New Issue
Block a user