From 5db3ec09e25875de089c31e356a8c1f51deb6a06 Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Mon, 19 Feb 2024 08:28:44 +0100 Subject: [PATCH] feat: Cart Customer link + create cart with customer (#6426) --- .../__tests__/cart/store/carts.spec.ts | 431 +++++++++++++----- .../__tests__/link-modules/cart-links.spec.ts | 41 +- .../inventory/create-inventory-items.ts | 4 +- .../cart/steps/find-or-create-customer.ts | 77 ++++ .../src/definition/cart/steps/index.ts | 1 + .../definition/cart/workflows/create-carts.ts | 32 +- .../src/definitions/cart-customer.ts | 28 ++ .../link-modules/src/definitions/index.ts | 1 + .../src/api-v2/store/carts/middlewares.ts | 10 + .../src/api-v2/store/carts/query-config.ts | 4 + .../medusa/src/api-v2/store/carts/route.ts | 19 +- .../src/api-v2/store/carts/validators.ts | 9 - packages/medusa/src/types/common.ts | 20 +- packages/types/src/cart/mutations.ts | 4 +- packages/types/src/cart/workflows.ts | 31 +- packages/types/src/customer/common.ts | 1 + 16 files changed, 544 insertions(+), 169 deletions(-) create mode 100644 packages/core-flows/src/definition/cart/steps/find-or-create-customer.ts create mode 100644 packages/link-modules/src/definitions/cart-customer.ts diff --git a/integration-tests/plugins/__tests__/cart/store/carts.spec.ts b/integration-tests/plugins/__tests__/cart/store/carts.spec.ts index 0b7995127e..e91eb226e5 100644 --- a/integration-tests/plugins/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/plugins/__tests__/cart/store/carts.spec.ts @@ -1,6 +1,11 @@ +import { + createCartWorkflow, + findOrCreateCustomerStepId, +} from "@medusajs/core-flows" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ICartModuleService, + ICustomerModuleService, IRegionModuleService, ISalesChannelModuleService, } from "@medusajs/types" @@ -10,18 +15,22 @@ 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" +import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" jest.setTimeout(50000) const env = { MEDUSA_FF_MEDUSA_V2: true } -describe("POST /store/carts", () => { +describe("Store Carts API", () => { let dbConnection let appContainer let shutdownServer let cartModuleService: ICartModuleService let regionModuleService: IRegionModuleService let scModuleService: ISalesChannelModuleService + let customerModule: ICustomerModuleService + + let defaultRegion beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) @@ -31,6 +40,7 @@ describe("POST /store/carts", () => { cartModuleService = appContainer.resolve(ModuleRegistrationName.CART) regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION) scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL) + customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER) }) afterAll(async () => { @@ -41,9 +51,14 @@ describe("POST /store/carts", () => { beforeEach(async () => { await adminSeeder(dbConnection) - // @ts-ignore 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 () => { @@ -51,157 +66,333 @@ describe("POST /store/carts", () => { await db.teardown() }) - it("should create and update a cart", async () => { - const region = await regionModuleService.create({ - name: "US", - currency_code: "usd", - }) - - const salesChannel = await scModuleService.create({ - name: "Webshop", - }) - - const api = useApi() as any - - const created = await api.post(`/store/carts`, { - email: "tony@stark.com", - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - }) - - expect(created.status).toEqual(200) - expect(created.data.cart).toEqual( - expect.objectContaining({ - id: created.data.cart.id, + describe("POST /store/carts", () => { + it("should create a cart", async () => { + const region = await regionModuleService.create({ + name: "US", currency_code: "usd", + }) + + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const api = useApi() as any + + const created = await api.post(`/store/carts`, { email: "tony@stark.com", - region: expect.objectContaining({ - id: region.id, - currency_code: "usd", - }), + currency_code: "usd", + region_id: region.id, sales_channel_id: salesChannel.id, }) - ) - const updated = await api.post(`/store/carts/${created.data.cart.id}`, { - email: "tony@stark-industries.com", + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: created.data.cart.id, + currency_code: "usd", + email: "tony@stark.com", + region: expect.objectContaining({ + id: region.id, + currency_code: "usd", + }), + sales_channel_id: salesChannel.id, + customer: expect.objectContaining({ + email: "tony@stark.com", + }), + }) + ) }) - expect(updated.status).toEqual(200) - expect(updated.data.cart).toEqual( - expect.objectContaining({ - id: updated.data.cart.id, + it("should create cart with customer from email", async () => { + const api = useApi() as any + + const created = await api.post(`/store/carts`, { currency_code: "usd", email: "tony@stark-industries.com", }) - ) - }) - it("should create cart with any region", async () => { - await regionModuleService.create({ - name: "US", - currency_code: "usd", + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: created.data.cart.id, + currency_code: "usd", + email: "tony@stark-industries.com", + customer: expect.objectContaining({ + id: expect.any(String), + email: "tony@stark-industries.com", + }), + }) + ) }) - const api = useApi() as any - const response = await api.post(`/store/carts`, { - email: "tony@stark.com", - currency_code: "usd", - }) - - expect(response.status).toEqual(200) - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: response.data.cart.id, + it("should create cart with any region", async () => { + await regionModuleService.create({ + name: "US", currency_code: "usd", - email: "tony@stark.com", - region: expect.objectContaining({ - id: expect.any(String), - }), }) - ) - }) - it("should create cart with region currency code", async () => { - await regionModuleService.create({ - name: "US", - currency_code: "usd", - }) - - const api = useApi() as any - const response = await api.post(`/store/carts`, { - email: "tony@stark.com", - }) - - expect(response.status).toEqual(200) - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: response.data.cart.id, - currency_code: "usd", - email: "tony@stark.com", - region: expect.objectContaining({ - id: expect.any(String), - }), - }) - ) - }) - - it("should throw when no regions exist", async () => { - const api = useApi() as any - - await expect( - api.post(`/store/carts`, { + const api = useApi() as any + const response = await api.post(`/store/carts`, { email: "tony@stark.com", currency_code: "usd", }) - ).rejects.toThrow() - }) - it("should get cart", async () => { - const region = await regionModuleService.create({ - name: "US", - currency_code: "usd", + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: response.data.cart.id, + currency_code: "usd", + email: "tony@stark.com", + region: expect.objectContaining({ + id: expect.any(String), + }), + }) + ) }) - const salesChannel = await scModuleService.create({ - name: "Webshop", + it("should create cart with region currency code", async () => { + const region = await regionModuleService.create({ + name: "US", + currency_code: "usd", + }) + + const api = useApi() as any + const response = await api.post(`/store/carts`, { + email: "tony@stark.com", + region_id: region.id, + }) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: response.data.cart.id, + currency_code: "usd", + email: "tony@stark.com", + region: expect.objectContaining({ + id: region.id, + }), + }) + ) }) - const cart = await cartModuleService.create({ - currency_code: "usd", - items: [ + it("should create cart with logged-in customer", async () => { + const { customer, jwt } = await createAuthenticatedCustomer(appContainer) + + const api = useApi() as any + const response = await api.post( + `/store/carts`, + {}, { - unit_price: 1000, - quantity: 1, - title: "Test item", - }, - ], - region_id: region.id, - sales_channel_id: salesChannel.id, + headers: { authorization: `Bearer ${jwt}` }, + } + ) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: response.data.cart.id, + currency_code: "dkk", + email: customer.email, + customer: expect.objectContaining({ + id: customer.id, + email: customer.email, + }), + }) + ) }) - const api = useApi() as any - const response = await api.get(`/store/carts/${cart.id}`) + it("should throw when no regions exist", async () => { + const api = useApi() as any - expect(response.status).toEqual(200) - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: cart.id, + 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 + + await expect( + api.post(`/store/carts`, { + foo: "bar", + }) + ).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", () => { + it("should create and update a cart", async () => { + const region = await regionModuleService.create({ + name: "US", currency_code: "usd", - items: expect.arrayContaining([ - expect.objectContaining({ + }) + + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const api = useApi() as any + + const created = await api.post(`/store/carts`, { + email: "tony@stark.com", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, + }) + + expect(created.status).toEqual(200) + expect(created.data.cart).toEqual( + expect.objectContaining({ + id: created.data.cart.id, + currency_code: "usd", + email: "tony@stark.com", + region: expect.objectContaining({ + id: region.id, + currency_code: "usd", + }), + sales_channel_id: salesChannel.id, + }) + ) + + const updated = await api.post(`/store/carts/${created.data.cart.id}`, { + email: "tony@stark-industries.com", + }) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + id: updated.data.cart.id, + currency_code: "usd", + email: "tony@stark-industries.com", + }) + ) + }) + }) + + describe("GET /store/carts", () => { + it("should get cart", async () => { + const region = await regionModuleService.create({ + name: "US", + currency_code: "usd", + }) + + const salesChannel = await scModuleService.create({ + name: "Webshop", + }) + + const cart = await cartModuleService.create({ + currency_code: "usd", + items: [ + { unit_price: 1000, quantity: 1, title: "Test item", - }), - ]), - region: expect.objectContaining({ - id: region.id, - currency_code: "usd", - }), + }, + ], + region_id: region.id, sales_channel_id: salesChannel.id, }) - ) + + const api = useApi() as any + const response = await api.get(`/store/carts/${cart.id}`) + + 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: 1000, + quantity: 1, + title: "Test item", + }), + ]), + region: expect.objectContaining({ + id: region.id, + currency_code: "usd", + }), + sales_channel_id: salesChannel.id, + }) + ) + }) }) }) diff --git a/integration-tests/plugins/__tests__/link-modules/cart-links.spec.ts b/integration-tests/plugins/__tests__/link-modules/cart-links.spec.ts index d7082805e6..562dc3f225 100644 --- a/integration-tests/plugins/__tests__/link-modules/cart-links.spec.ts +++ b/integration-tests/plugins/__tests__/link-modules/cart-links.spec.ts @@ -1,6 +1,7 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ICartModuleService, + ICustomerModuleService, IRegionModuleService, ISalesChannelModuleService, } from "@medusajs/types" @@ -18,9 +19,10 @@ describe("Cart links", () => { let appContainer let shutdownServer let cartModuleService: ICartModuleService + let regionModule: IRegionModuleService + let customerModule: ICustomerModuleService let scModuleService: ISalesChannelModuleService let remoteQuery - let regionModule: IRegionModuleService beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) @@ -28,6 +30,8 @@ describe("Cart links", () => { shutdownServer = await startBootstrapApp({ cwd, env }) appContainer = getContainer() cartModuleService = appContainer.resolve(ModuleRegistrationName.CART) + regionModule = appContainer.resolve(ModuleRegistrationName.REGION) + customerModule = appContainer.resolve(ModuleRegistrationName.CUSTOMER) scModuleService = appContainer.resolve(ModuleRegistrationName.SALES_CHANNEL) regionModule = appContainer.resolve(ModuleRegistrationName.REGION) remoteQuery = appContainer.resolve("remoteQuery") @@ -49,12 +53,16 @@ describe("Cart links", () => { await db.teardown() }) - it("should query carts, sales channels, regions with remote query", async () => { + it("should query carts, sales channels, customers, regions with remote query", async () => { const region = await regionModule.create({ name: "Region", currency_code: "usd", }) + const customer = await customerModule.create({ + email: "tony@stark.com", + }) + const salesChannel = await scModuleService.create({ name: "Webshop", }) @@ -64,15 +72,19 @@ describe("Cart links", () => { currency_code: "usd", region_id: region.id, sales_channel_id: salesChannel.id, + customer_id: customer.id, }) const carts = await remoteQuery({ cart: { fields: ["id"], - sales_channel: { + region: { fields: ["id"], }, - region: { + customer: { + fields: ["id"], + }, + sales_channel: { fields: ["id"], }, }, @@ -87,6 +99,15 @@ describe("Cart links", () => { }, }) + const customers = await remoteQuery({ + customer: { + fields: ["id"], + carts: { + fields: ["id"], + }, + }, + }) + const regions = await remoteQuery({ region: { fields: ["id"], @@ -100,6 +121,7 @@ describe("Cart links", () => { expect.arrayContaining([ expect.objectContaining({ id: cart.id, + customer: expect.objectContaining({ id: customer.id }), sales_channel: expect.objectContaining({ id: salesChannel.id }), region: expect.objectContaining({ id: region.id }), }), @@ -117,6 +139,17 @@ describe("Cart links", () => { ]) ) + expect(customers).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: customer.id, + carts: expect.arrayContaining([ + expect.objectContaining({ id: cart.id }), + ]), + }), + ]) + ) + expect(regions).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/integration-tests/plugins/__tests__/workflows/inventory/create-inventory-items.ts b/integration-tests/plugins/__tests__/workflows/inventory/create-inventory-items.ts index c8c6179180..293fa75ea3 100644 --- a/integration-tests/plugins/__tests__/workflows/inventory/create-inventory-items.ts +++ b/integration-tests/plugins/__tests__/workflows/inventory/create-inventory-items.ts @@ -1,9 +1,9 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { CreateInventoryItemActions, createInventoryItems, } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IInventoryService, WorkflowTypes } from "@medusajs/types" import { pipe } from "@medusajs/workflows-sdk" import path from "path" diff --git a/packages/core-flows/src/definition/cart/steps/find-or-create-customer.ts b/packages/core-flows/src/definition/cart/steps/find-or-create-customer.ts new file mode 100644 index 0000000000..3ccc4ebbde --- /dev/null +++ b/packages/core-flows/src/definition/cart/steps/find-or-create-customer.ts @@ -0,0 +1,77 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { CustomerDTO, ICustomerModuleService } from "@medusajs/types" +import { validateEmail } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + customerId?: string + email?: string +} + +interface StepOutput { + customer?: CustomerDTO + email?: string +} + +interface StepCompensateInput { + customer?: CustomerDTO + customerWasCreated: boolean +} + +export const findOrCreateCustomerStepId = "find-or-create-customer" +export const findOrCreateCustomerStep = createStep( + findOrCreateCustomerStepId, + async (data: StepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + const customerData: StepOutput = {} + let customerWasCreated = false + + if (data.customerId) { + const customer = await service.retrieve(data.customerId) + customerData.customer = customer + customerData.email = customer.email + + return new StepResponse(customerData, { + customerWasCreated, + }) + } + + if (data.email) { + const validatedEmail = validateEmail(data.email) + + let [customer] = await service.list({ + email: validatedEmail, + has_account: false, + }) + + if (!customer) { + customer = await service.create({ email: validatedEmail }) + customerWasCreated = true + } + + customerData.customer = customer + customerData.email = customer.email + } + + return new StepResponse(customerData, { + customer: customerData.customer, + customerWasCreated, + }) + }, + async (compData, { container }) => { + const { customer, customerWasCreated } = compData as StepCompensateInput + + if (!customerWasCreated || !customer?.id) { + return + } + + const service = container.resolve( + ModuleRegistrationName.CUSTOMER + ) + + await service.delete(customer.id) + } +) diff --git a/packages/core-flows/src/definition/cart/steps/index.ts b/packages/core-flows/src/definition/cart/steps/index.ts index f4cf25ebf7..2803a6a741 100644 --- a/packages/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core-flows/src/definition/cart/steps/index.ts @@ -1,4 +1,5 @@ export * from "./create-carts" export * from "./find-one-or-any-region" +export * from "./find-or-create-customer" export * from "./update-carts" diff --git a/packages/core-flows/src/definition/cart/workflows/create-carts.ts b/packages/core-flows/src/definition/cart/workflows/create-carts.ts index 95605c4f1d..724a51e3d6 100644 --- a/packages/core-flows/src/definition/cart/workflows/create-carts.ts +++ b/packages/core-flows/src/definition/cart/workflows/create-carts.ts @@ -4,28 +4,44 @@ import { createWorkflow, transform, } from "@medusajs/workflows-sdk" -import { createCartsStep, findOneOrAnyRegionStep } from "../steps" +import { + createCartsStep, + findOneOrAnyRegionStep, + findOrCreateCustomerStep, +} from "../steps" export const createCartWorkflowId = "create-cart" export const createCartWorkflow = createWorkflow( createCartWorkflowId, - ( - input: WorkflowData - ): WorkflowData => { + (input: WorkflowData): WorkflowData => { const region = findOneOrAnyRegionStep({ regionId: input.region_id, }) - const cartInput = transform({ input, region }, (data) => { - return { + const customerData = findOrCreateCustomerStep({ + customerId: input.customer_id, + email: input.email, + }) + + const cartInput = transform({ input, region, customerData }, (data) => { + const data_ = { ...data.input, - currency_code: data?.input.currency_code || data.region.currency_code, + currency_code: data.input.currency_code ?? data.region.currency_code, region_id: data.region.id, } + + if (data.customerData.customer?.id) { + data_.customer_id = data.customerData.customer.id + data_.email = data.input?.email ?? data.customerData.customer.email + } + + return data_ }) + // TODO: Add line items + const cart = createCartsStep([cartInput]) - return cart + return cart[0] } ) diff --git a/packages/link-modules/src/definitions/cart-customer.ts b/packages/link-modules/src/definitions/cart-customer.ts new file mode 100644 index 0000000000..b32bb9eff7 --- /dev/null +++ b/packages/link-modules/src/definitions/cart-customer.ts @@ -0,0 +1,28 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" + +export const CartCustomer: ModuleJoinerConfig = { + isLink: true, + isReadOnlyLink: true, + extends: [ + { + serviceName: Modules.CART, + relationship: { + serviceName: Modules.CUSTOMER, + primaryKey: "id", + foreignKey: "customer_id", + alias: "customer", + }, + }, + { + serviceName: Modules.CUSTOMER, + relationship: { + serviceName: Modules.CART, + primaryKey: "customer_id", + foreignKey: "id", + alias: "carts", + isList: true, + }, + }, + ], +} diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 8bf0ea9e94..bc9f1e53aa 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -1,3 +1,4 @@ +export * from "./cart-customer" export * from "./cart-region" export * from "./cart-sales-channel" export * from "./inventory-level-stock-location" diff --git a/packages/medusa/src/api-v2/store/carts/middlewares.ts b/packages/medusa/src/api-v2/store/carts/middlewares.ts index e811b40b9e..b224a56666 100644 --- a/packages/medusa/src/api-v2/store/carts/middlewares.ts +++ b/packages/medusa/src/api-v2/store/carts/middlewares.ts @@ -1,5 +1,6 @@ import { transformBody, transformQuery } from "../../../api/middlewares" import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" import { StoreGetCartsCartParams, @@ -8,6 +9,15 @@ import { } from "./validators" export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: "ALL", + matcher: "/store/carts*", + middlewares: [ + authenticate("store", ["session", "bearer"], { + allowUnauthenticated: true, + }), + ], + }, { method: ["GET"], matcher: "/store/carts/:id", diff --git a/packages/medusa/src/api-v2/store/carts/query-config.ts b/packages/medusa/src/api-v2/store/carts/query-config.ts index 1490036c55..4139bcee31 100644 --- a/packages/medusa/src/api-v2/store/carts/query-config.ts +++ b/packages/medusa/src/api-v2/store/carts/query-config.ts @@ -10,6 +10,8 @@ export const defaultStoreCartFields = [ "items.title", "items.quantity", "items.unit_price", + "customer.id", + "customer.email", "shipping_address.id", "shipping_address.first_name", "shipping_address.last_name", @@ -39,6 +41,7 @@ export const defaultStoreCartFields = [ export const defaultStoreCartRelations = [ "items", "region", + "customer", "shipping_address", "billing_address", "shipping_methods", @@ -47,6 +50,7 @@ export const defaultStoreCartRelations = [ export const allowedRelations = [ "items", "region", + "customer", "shipping_address", "billing_address", "shipping_methods", diff --git a/packages/medusa/src/api-v2/store/carts/route.ts b/packages/medusa/src/api-v2/store/carts/route.ts index d530a06487..971e559e7e 100644 --- a/packages/medusa/src/api-v2/store/carts/route.ts +++ b/packages/medusa/src/api-v2/store/carts/route.ts @@ -1,14 +1,23 @@ import { createCartWorkflow } from "@medusajs/core-flows" -import { CreateCartDTO } from "@medusajs/types" +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 workflow = createCartWorkflow(req.scope) + const input = req.validatedBody as StorePostCartReq + const workflowInput: CreateCartWorkflowInputDTO = { + ...input, + } - const { result, errors } = await workflow.run({ - input: req.validatedBody as CreateCartDTO, + // If the customer is logged in, we auto-assign them to the cart + if (req.auth_user?.app_metadata?.customer_id) { + workflowInput.customer_id = req.auth_user!.app_metadata?.customer_id + } + + const { result, errors } = await createCartWorkflow(req.scope).run({ + input: workflowInput, throwOnError: false, }) @@ -18,7 +27,7 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { const remoteQuery = req.scope.resolve("remoteQuery") - const variables = { id: result[0].id } + const variables = { id: result.id } const query = remoteQueryObjectFromString({ entryPoint: "cart", diff --git a/packages/medusa/src/api-v2/store/carts/validators.ts b/packages/medusa/src/api-v2/store/carts/validators.ts index f0dda2d5fd..da22a235f6 100644 --- a/packages/medusa/src/api-v2/store/carts/validators.ts +++ b/packages/medusa/src/api-v2/store/carts/validators.ts @@ -29,15 +29,10 @@ export class StorePostCartReq { @IsString() region_id?: string - @IsOptional() - @IsString() - customer_id?: string - @IsOptional() @IsString() email?: string - // TODO: Remove in favor of using region currencies, as in the core @IsOptional() @IsString() currency_code?: string @@ -74,10 +69,6 @@ export class StorePostCartsCartReq { @IsType([AddressPayload, String]) shipping_address?: AddressPayload | string - @IsString() - @IsOptional() - customer_id?: string - @IsEmail() @IsOptional() sales_channel_id?: string diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index 707c537cde..9b7a967e75 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -1,5 +1,13 @@ import "reflect-metadata" +import { Transform, Type } from "class-transformer" +import { + IsDate, + IsNumber, + IsObject, + IsOptional, + IsString, +} from "class-validator" import { FindManyOptions, FindOneOptions, @@ -8,20 +16,12 @@ import { FindOptionsWhere, OrderByCondition, } from "typeorm" -import { - IsDate, - IsNumber, - IsObject, - IsOptional, - IsString, -} from "class-validator" -import { Transform, Type } from "class-transformer" -import { BaseEntity } from "../interfaces" -import { ClassConstructor } from "./global" import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" +import { BaseEntity } from "../interfaces" import { transformDate } from "../utils/validators/date-transform" +import { ClassConstructor } from "./global" /** * Utility type used to remove some optional attributes (coming from K) from a type T diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index 92a7fe0c71..0a20438f1e 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -33,8 +33,8 @@ export interface CreateCartDTO { currency_code: string shipping_address_id?: string billing_address_id?: string - shipping_address?: CreateAddressDTO | UpdateAddressDTO - billing_address?: CreateAddressDTO | UpdateAddressDTO + shipping_address?: CreateAddressDTO | string + billing_address?: CreateAddressDTO | string metadata?: Record items?: CreateLineItemDTO[] diff --git a/packages/types/src/cart/workflows.ts b/packages/types/src/cart/workflows.ts index 41df8d2d62..6011913a93 100644 --- a/packages/types/src/cart/workflows.ts +++ b/packages/types/src/cart/workflows.ts @@ -1,20 +1,33 @@ -import { - CreateAddressDTO, - CreateLineItemDTO, - UpdateAddressDTO, -} from "./mutations" +export interface CreateCartLineItemDTO { + variant_id: string + quantity: number +} + +export interface CreateCartAddressDTO { + first_name?: string + last_name?: string + phone?: string + company?: string + address_1?: string + address_2?: string + city?: string + country_code?: string + province?: string + postal_code?: string + metadata?: Record +} export interface CreateCartWorkflowInputDTO { region_id?: string customer_id?: string sales_channel_id?: string email?: string - currency_code: string + currency_code?: string shipping_address_id?: string billing_address_id?: string - shipping_address?: CreateAddressDTO | UpdateAddressDTO - billing_address?: CreateAddressDTO | UpdateAddressDTO + shipping_address?: CreateCartAddressDTO | string + billing_address?: CreateCartAddressDTO | string metadata?: Record - items?: CreateLineItemDTO[] + items?: CreateCartLineItemDTO[] } diff --git a/packages/types/src/customer/common.ts b/packages/types/src/customer/common.ts index 0240c7e4c0..47a0212de6 100644 --- a/packages/types/src/customer/common.ts +++ b/packages/types/src/customer/common.ts @@ -78,6 +78,7 @@ export interface FilterableCustomerProps company_name?: string | string[] | OperatorMap | null first_name?: string | string[] | OperatorMap | null last_name?: string | string[] | OperatorMap | null + has_account?: boolean | OperatorMap created_by?: string | string[] | null created_at?: OperatorMap updated_at?: OperatorMap