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:
1 parent
ac86362e81
commit
7ebe885ec9
26 files changed
+1100
-157
No files matched your search
@@ -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 { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import {
|
import {
|
||||||
ICartModuleService,
|
ICartModuleService,
|
||||||
ICustomerModuleService,
|
ICustomerModuleService,
|
||||||
|
IPricingModuleService,
|
||||||
|
IProductModuleService,
|
||||||
IRegionModuleService,
|
IRegionModuleService,
|
||||||
ISalesChannelModuleService,
|
ISalesChannelModuleService,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
@@ -29,6 +27,9 @@ describe("Store Carts API", () => {
|
|||||||
let regionModuleService: IRegionModuleService
|
let regionModuleService: IRegionModuleService
|
||||||
let scModuleService: ISalesChannelModuleService
|
let scModuleService: ISalesChannelModuleService
|
||||||
let customerModule: ICustomerModuleService
|
let customerModule: ICustomerModuleService
|
||||||
|
let productModule: IProductModuleService
|
||||||
|
let pricingModule: IPricingModuleService
|
||||||
|
let remoteLink
|
||||||
|
|
||||||
let defaultRegion
|
let defaultRegion
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ describe("Store Carts API", () => {
|
|||||||
regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION)
|
regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||||
scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL)
|
scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL)
|
||||||
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
|
customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER)
|
||||||
|
productModule = appContainer.resolve(ModuleRegistrationName.PRODUCT)
|
||||||
|
pricingModule = appContainer.resolve(ModuleRegistrationName.PRICING)
|
||||||
|
remoteLink = appContainer.resolve("remoteLink")
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -51,7 +55,6 @@ describe("Store Carts API", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await adminSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
// @ts-ignore
|
|
||||||
await regionModuleService.createDefaultCountriesAndCurrencies()
|
await regionModuleService.createDefaultCountriesAndCurrencies()
|
||||||
|
|
||||||
// Here, so we don't have to create a region for each test
|
// Here, so we don't have to create a region for each test
|
||||||
@@ -77,6 +80,58 @@ describe("Store Carts API", () => {
|
|||||||
name: "Webshop",
|
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 api = useApi() as any
|
||||||
|
|
||||||
const created = await api.post(`/store/carts`, {
|
const created = await api.post(`/store/carts`, {
|
||||||
@@ -84,6 +139,16 @@ describe("Store Carts API", () => {
|
|||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
region_id: region.id,
|
region_id: region.id,
|
||||||
sales_channel_id: salesChannel.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)
|
expect(created.status).toEqual(200)
|
||||||
@@ -100,6 +165,16 @@ describe("Store Carts API", () => {
|
|||||||
customer: expect.objectContaining({
|
customer: expect.objectContaining({
|
||||||
email: "tony@stark.com",
|
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 () => {
|
it("should respond 400 bad request on unknown props", async () => {
|
||||||
const api = useApi() as any
|
const api = useApi() as any
|
||||||
|
|
||||||
@@ -224,93 +286,6 @@ describe("Store Carts API", () => {
|
|||||||
})
|
})
|
||||||
).rejects.toThrow()
|
).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", () => {
|
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 { initDb, useDb } from "../../../environment-helpers/use-db"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StepResponse,
|
|
||||||
WorkflowData,
|
|
||||||
createStep,
|
createStep,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
|
StepResponse,
|
||||||
|
WorkflowData,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import { AxiosInstance } from "axios"
|
import { AxiosInstance } from "axios"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -200,9 +200,9 @@ export const workflowEngineTestSuite = (env, extraParams = {}) => {
|
|||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
invoke: {
|
invoke: {
|
||||||
"my-step": {
|
"my-step": {
|
||||||
__type: "WorkflowWorkflowData",
|
__type: "Symbol(WorkflowWorkflowData)",
|
||||||
output: {
|
output: {
|
||||||
__type: "WorkflowStepResponse",
|
__type: "Symbol(WorkflowStepResponse)",
|
||||||
output: {
|
output: {
|
||||||
result: "abc",
|
result: "abc",
|
||||||
},
|
},
|
||||||
@@ -248,7 +248,7 @@ export const workflowEngineTestSuite = (env, extraParams = {}) => {
|
|||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
invoke: expect.objectContaining({
|
invoke: expect.objectContaining({
|
||||||
"my-step-async": {
|
"my-step-async": {
|
||||||
__type: "WorkflowStepResponse",
|
__type: "Symbol(WorkflowStepResponse)",
|
||||||
output: {
|
output: {
|
||||||
all: "good",
|
all: "good",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
import { IRegionModuleService } from "@medusajs/types"
|
import { IRegionModuleService } from "@medusajs/types"
|
||||||
|
import { MedusaError } from "@medusajs/utils"
|
||||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
export const findOneOrAnyRegionStepId = "find-one-or-any-region"
|
export const findOneOrAnyRegionStepId = "find-one-or-any-region"
|
||||||
@@ -14,7 +15,10 @@ export const findOneOrAnyRegionStep = createStep(
|
|||||||
const regions = await service.list({})
|
const regions = await service.list({})
|
||||||
|
|
||||||
if (!regions?.length) {
|
if (!regions?.length) {
|
||||||
throw Error("No regions found")
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"No regions found"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StepResponse(regions[0])
|
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-carts"
|
||||||
export * from "./create-line-item-adjustments"
|
export * from "./create-line-item-adjustments"
|
||||||
export * from "./create-shipping-method-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-or-create-customer"
|
||||||
export * from "./find-sales-channel"
|
export * from "./find-sales-channel"
|
||||||
export * from "./get-actions-to-compute-from-promotions"
|
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 "./prepare-adjustments-from-promotion-actions"
|
||||||
export * from "./remove-line-item-adjustments"
|
export * from "./remove-line-item-adjustments"
|
||||||
export * from "./remove-shipping-method-adjustments"
|
export * from "./remove-shipping-method-adjustments"
|
||||||
export * from "./retrieve-cart"
|
export * from "./retrieve-cart"
|
||||||
export * from "./update-carts"
|
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 { CartDTO, CreateCartWorkflowInputDTO } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
WorkflowData,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
WorkflowData,
|
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
createCartsStep,
|
createCartsStep,
|
||||||
findOneOrAnyRegionStep,
|
findOneOrAnyRegionStep,
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
findSalesChannelStep,
|
findSalesChannelStep,
|
||||||
|
getVariantPriceSetsStep,
|
||||||
|
getVariantsStep,
|
||||||
|
validateVariantsExistStep,
|
||||||
} from "../steps"
|
} 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 createCartWorkflowId = "create-cart"
|
||||||
export const createCartWorkflow = createWorkflow(
|
export const createCartWorkflow = createWorkflow(
|
||||||
createCartWorkflowId,
|
createCartWorkflowId,
|
||||||
(input: WorkflowData<CreateCartWorkflowInputDTO>): WorkflowData<CartDTO> => {
|
(input: WorkflowData<CreateCartWorkflowInputDTO>): WorkflowData<CartDTO> => {
|
||||||
|
const variantIds = transform({ input }, (data) => {
|
||||||
|
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||||
|
})
|
||||||
|
|
||||||
const [salesChannel, region, customerData] = parallelize(
|
const [salesChannel, region, customerData] = parallelize(
|
||||||
findSalesChannelStep({
|
findSalesChannelStep({
|
||||||
salesChannelId: input.sales_channel_id,
|
salesChannelId: input.sales_channel_id,
|
||||||
@@ -26,9 +40,27 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
findOrCreateCustomerStep({
|
findOrCreateCustomerStep({
|
||||||
customerId: input.customer_id,
|
customerId: input.customer_id,
|
||||||
email: input.email,
|
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(
|
const cartInput = transform(
|
||||||
{ input, region, customerData, salesChannel },
|
{ input, region, customerData, salesChannel },
|
||||||
(data) => {
|
(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 lineItems = transform({ priceSets, input, variants }, (data) => {
|
||||||
const cart = createCartsStep([cartInput])
|
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 "./create-carts"
|
||||||
export * from "./update-cart-promotions"
|
export * from "./update-cart-promotions"
|
||||||
export * from "./update-carts"
|
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 { transformBody, transformQuery } from "../../../api/middlewares"
|
||||||
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
|
import { StorePostCartsCartLineItemsReq } from "./[id]/line-items/validators"
|
||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
import {
|
import {
|
||||||
StoreDeleteCartsCartPromotionsReq,
|
StoreDeleteCartsCartPromotionsReq,
|
||||||
@@ -40,6 +41,11 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
matcher: "/store/carts/:id",
|
matcher: "/store/carts/:id",
|
||||||
middlewares: [transformBody(StorePostCartsCartReq)],
|
middlewares: [transformBody(StorePostCartsCartReq)],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/store/carts/:id/line-items",
|
||||||
|
middlewares: [transformBody(StorePostCartsCartLineItemsReq)],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/store/carts/:id/promotions",
|
matcher: "/store/carts/:id/promotions",
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ import { CreateCartWorkflowInputDTO } from "@medusajs/types"
|
|||||||
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
import { remoteQueryObjectFromString } from "@medusajs/utils"
|
||||||
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
import { MedusaRequest, MedusaResponse } from "../../../types/routing"
|
||||||
import { defaultStoreCartFields } from "../carts/query-config"
|
import { defaultStoreCartFields } from "../carts/query-config"
|
||||||
import { StorePostCartReq } from "./validators"
|
|
||||||
|
|
||||||
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
|
||||||
const input = req.validatedBody as StorePostCartReq
|
const workflowInput = req.validatedBody as CreateCartWorkflowInputDTO
|
||||||
const workflowInput: CreateCartWorkflowInputDTO = {
|
|
||||||
...input,
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the customer is logged in, we auto-assign them to the cart
|
// If the customer is logged in, we auto-assign them to the cart
|
||||||
if (req.auth_user?.app_metadata?.customer_id) {
|
if (req.auth_user?.app_metadata?.customer_id) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextFunction, Request, Response } from "express"
|
import { NextFunction, Request, Response } from "express"
|
||||||
|
|
||||||
import { Logger } from "../../types/global"
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { Logger } from "../../types/global"
|
||||||
import { formatException } from "../../utils"
|
import { formatException } from "../../utils"
|
||||||
|
|
||||||
const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError"
|
const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { handlePostgresDatabaseError } from "@medusajs/utils"
|
||||||
import { AwilixContainer } from "awilix"
|
import { AwilixContainer } from "awilix"
|
||||||
import {
|
import {
|
||||||
DataSource,
|
DataSource,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { ConfigModule } from "../types/global"
|
import { ConfigModule } from "../types/global"
|
||||||
import "../utils/naming-strategy"
|
import "../utils/naming-strategy"
|
||||||
import { handlePostgresDatabaseError } from "@medusajs/utils"
|
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
configModule: ConfigModule
|
configModule: ConfigModule
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import {
|
|||||||
PriceSetMoneyAmountDTO,
|
PriceSetMoneyAmountDTO,
|
||||||
RemoteQueryFunction,
|
RemoteQueryFunction,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
|
||||||
CustomerService,
|
|
||||||
ProductVariantService,
|
|
||||||
RegionService,
|
|
||||||
TaxProviderService,
|
|
||||||
} from "."
|
|
||||||
import {
|
import {
|
||||||
FlagRouter,
|
FlagRouter,
|
||||||
MedusaV2Flag,
|
MedusaV2Flag,
|
||||||
promiseAll,
|
promiseAll,
|
||||||
removeNullish,
|
removeNullish,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
CustomerService,
|
||||||
|
ProductVariantService,
|
||||||
|
RegionService,
|
||||||
|
TaxProviderService,
|
||||||
|
} from "."
|
||||||
import {
|
import {
|
||||||
IPriceSelectionStrategy,
|
IPriceSelectionStrategy,
|
||||||
PriceSelectionContext,
|
PriceSelectionContext,
|
||||||
@@ -36,11 +36,11 @@ import {
|
|||||||
TaxedPricing,
|
TaxedPricing,
|
||||||
} from "../types/pricing"
|
} from "../types/pricing"
|
||||||
|
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
|
||||||
import { TaxServiceRate } from "../types/tax-service"
|
import { TaxServiceRate } from "../types/tax-service"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
|
||||||
import { calculatePriceTaxAmount } from "../utils"
|
import { calculatePriceTaxAmount } from "../utils"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
|
|||||||
@@ -1,6 +1,35 @@
|
|||||||
export interface CreateCartLineItemDTO {
|
import { CartDTO } from "./common"
|
||||||
variant_id: string
|
|
||||||
|
export interface CreateCartCreateLineItemDTO {
|
||||||
quantity: number
|
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 {
|
export interface CreateCartAddressDTO {
|
||||||
@@ -29,5 +58,10 @@ export interface CreateCartWorkflowInputDTO {
|
|||||||
billing_address?: CreateCartAddressDTO | string
|
billing_address?: CreateCartAddressDTO | string
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
|
||||||
items?: CreateCartLineItemDTO[]
|
items?: CreateCartCreateLineItemDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddToCartWorkflowInputDTO {
|
||||||
|
items: CreateCartCreateLineItemDTO[]
|
||||||
|
cart: CartDTO
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ export interface ProductDTO {
|
|||||||
*
|
*
|
||||||
* @expandable
|
* @expandable
|
||||||
*/
|
*/
|
||||||
type: ProductTypeDTO[]
|
type: ProductTypeDTO
|
||||||
/**
|
/**
|
||||||
* The associated product tags.
|
* The associated product tags.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
export const SymbolMedusaWorkflowComposerContext = Symbol.for(
|
export const SymbolMedusaWorkflowComposerContext = Symbol.for(
|
||||||
"MedusaWorkflowComposerContext"
|
"MedusaWorkflowComposerContext"
|
||||||
)
|
).toString()
|
||||||
export const SymbolInputReference = Symbol.for("WorkflowInputReference")
|
export const SymbolInputReference = Symbol.for(
|
||||||
export const SymbolWorkflowStep = Symbol.for("WorkflowStep")
|
"WorkflowInputReference"
|
||||||
export const SymbolWorkflowHook = Symbol.for("WorkflowHook")
|
).toString()
|
||||||
export const SymbolWorkflowWorkflowData = Symbol.for("WorkflowWorkflowData")
|
export const SymbolWorkflowStep = Symbol.for("WorkflowStep").toString()
|
||||||
export const SymbolWorkflowStepResponse = Symbol.for("WorkflowStepResponse")
|
export const SymbolWorkflowHook = Symbol.for("WorkflowHook").toString()
|
||||||
export const SymbolWorkflowStepBind = Symbol.for("WorkflowStepBind")
|
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(
|
export const SymbolWorkflowStepTransformer = Symbol.for(
|
||||||
"WorkflowStepTransformer"
|
"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") {
|
if (key === "__type" && typeof value === "symbol") {
|
||||||
return Symbol.keyFor(value)
|
return Symbol.keyFor(value)
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio
|
|||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}*/
|
||||||
|
|
||||||
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
||||||
return this.storage.get(key)
|
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)
|
const parsedData = JSON.parse(stringifiedData)
|
||||||
|
|
||||||
if (hasFinished && !retentionTime) {
|
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") {
|
if (key === "__type" && typeof value === "symbol") {
|
||||||
return Symbol.keyFor(value)
|
return Symbol.keyFor(value)
|
||||||
}
|
}
|
||||||
@@ -113,12 +113,12 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}*/
|
||||||
|
|
||||||
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
async get(key: string): Promise<TransactionCheckpoint | undefined> {
|
||||||
const data = await this.redisClient.get(key)
|
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[]> {
|
async list(): Promise<TransactionCheckpoint[]> {
|
||||||
@@ -129,7 +129,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const data = await this.redisClient.get(key)
|
const data = await this.redisClient.get(key)
|
||||||
if (data) {
|
if (data) {
|
||||||
transactions.push(JSON.parse(data, this.jsonWithSymbol))
|
transactions.push(JSON.parse(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transactions
|
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)
|
const parsedData = JSON.parse(stringifiedData)
|
||||||
|
|
||||||
if (!hasFinished) {
|
if (!hasFinished) {
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import { Context, MedusaContainer } from "@medusajs/types"
|
|||||||
export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
|
export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
|
||||||
(this: CreateWorkflowComposerContext) => WorkflowData<TOutput>
|
(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.
|
* 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 []
|
export type StepFunction<TInput, TOutput = unknown> = (keyof TInput extends []
|
||||||
? // Function that doesn't expect any input
|
? // Function that doesn't expect any input
|
||||||
{
|
{
|
||||||
(): WorkflowData<TOutput>
|
(): WorkflowData<TOutput> & StepFunctionReturnConfig<TOutput>
|
||||||
}
|
}
|
||||||
: // function that expects an input object
|
: // function that expects an input object
|
||||||
{
|
{
|
||||||
(input: WorkflowData<TInput> | TInput): WorkflowData<TOutput>
|
(input: WorkflowData<TInput> | TInput): WorkflowData<TOutput> &
|
||||||
|
StepFunctionReturnConfig<TOutput>
|
||||||
}) &
|
}) &
|
||||||
WorkflowDataProperties<TOutput>
|
WorkflowDataProperties<TOutput>
|
||||||
|
|
||||||
export type WorkflowDataProperties<T = unknown> = {
|
export type WorkflowDataProperties<T = unknown> = {
|
||||||
__type: Symbol
|
__type: string
|
||||||
__step__: string
|
__step__: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +47,11 @@ export type WorkflowDataProperties<T = unknown> = {
|
|||||||
*
|
*
|
||||||
* @typeParam T - The type of a step's input or result.
|
* @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 & WorkflowDataProperties<T>) &
|
||||||
T &
|
T &
|
||||||
|
|||||||
Reference in new issue
Block a user