diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 8f244080f7..8b37d7461d 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -1,3 +1,4 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { Modules, PriceListStatus, @@ -6,39 +7,19 @@ import { PromotionRuleOperator, PromotionType, } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { createAdminUser, generatePublishableKey, generateStoreHeaders, } from "../../../../helpers/create-admin-user" import { setupTaxStructure } from "../../../../modules/__tests__/fixtures" +import { createAuthenticatedCustomer } from "../../../../modules/helpers/create-authenticated-customer" jest.setTimeout(100000) const env = { MEDUSA_FF_MEDUSA_V2: true } const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } -const generateStoreHeadersWithCustomer = async ({ - api, - storeHeaders, - customer, -}) => { - const registeredCustomerToken = ( - await api.post("/auth/customer/emailpass/register", { - email: customer.email, - password: "password", - }) - ).data.token - - return { - headers: { - ...storeHeaders.headers, - authorization: `Bearer ${registeredCustomerToken}`, - }, - } -} - const shippingAddressData = { address_1: "test address 1", address_2: "test address 2", @@ -136,23 +117,20 @@ medusaIntegrationTestRunner({ const publishableKey = await generatePublishableKey(appContainer) storeHeaders = generateStoreHeaders({ publishableKey }) - customer = ( - await api.post( - "/admin/customers", - { - first_name: "tony", - email: "tony@stark-industries.com", - }, - adminHeaders - ) - ).data.customer - - storeHeadersWithCustomer = await generateStoreHeadersWithCustomer({ - storeHeaders, - api, - customer, + const result = await createAuthenticatedCustomer(appContainer, { + first_name: "tony", + last_name: "stark", + email: "tony@stark-industries.com", }) + customer = result.customer + storeHeadersWithCustomer = { + headers: { + ...storeHeaders.headers, + authorization: `Bearer ${result.jwt}`, + }, + } + await setupTaxStructure(appContainer.resolve(Modules.TAX)) region = ( @@ -579,23 +557,23 @@ medusaIntegrationTestRunner({ }) describe("POST /store/carts/:id", () => { - let otherRegion + let otherRegion, cartWithCustomer beforeEach(async () => { - cart = ( - await api.post( - `/store/carts`, - { - email: "tony@stark.com", - currency_code: "usd", - sales_channel_id: salesChannel.id, - region_id: region.id, - shipping_address: shippingAddressData, - items: [{ variant_id: product.variants[0].id, quantity: 1 }], - promo_codes: [promotion.code], - }, - storeHeadersWithCustomer - ) + const cartData = { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + promo_codes: [promotion.code], + } + + cart = (await api.post(`/store/carts`, cartData, storeHeaders)).data + .cart + + cartWithCustomer = ( + await api.post(`/store/carts`, cartData, storeHeadersWithCustomer) ).data.cart otherRegion = ( @@ -751,7 +729,7 @@ medusaIntegrationTestRunner({ it("should not generate tax lines if automatic taxes is false", async () => { let updated = await api.post( `/store/carts/${cart.id}`, - { email: "another@tax.com" }, + {}, storeHeaders ) @@ -776,7 +754,7 @@ medusaIntegrationTestRunner({ updated = await api.post( `/store/carts/${cart.id}`, - { email: "another@tax.com", region_id: noAutomaticRegion.id }, + { region_id: noAutomaticRegion.id }, storeHeaders ) @@ -1236,6 +1214,103 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should update email if cart customer_id is not set", async () => { + const updated = await api.post( + `/store/carts/${cart.id}`, + { email: "tony@stark.com" }, + storeHeaders + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + email: "tony@stark.com", + customer: expect.objectContaining({ + email: "tony@stark.com", + }), + }) + ) + }) + + it("should update customer_id if cart customer_id if not already set", async () => { + const updated = await api.post( + `/store/carts/${cart.id}`, + { customer_id: customer.id }, + storeHeadersWithCustomer + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + email: customer.email, + customer: expect.objectContaining({ + id: customer.id, + email: customer.email, + }), + }) + ) + }) + + it("should throw when trying to set customer_id if customer is not logged in", async () => { + const { response } = await api + .post( + `/store/carts/${cartWithCustomer.id}`, + { customer_id: customer.id }, + storeHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data.message).toEqual( + "auth_customer_id is required when customer_id is set" + ) + }) + + it("should throw when trying to set customer_id if customer_id is already set", async () => { + const newCustomer = ( + await api.post( + "/admin/customers", + { + first_name: "new tony", + email: "new-tony@stark-industries.com", + }, + adminHeaders + ) + ).data.customer + + const { response } = await api + .post( + `/store/carts/${cartWithCustomer.id}`, + { customer_id: newCustomer.id }, + storeHeadersWithCustomer + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data.message).toEqual( + "Cannot update cart customer when customer_id is set" + ) + }) + + it("should update email when email is already set and customer is logged in", async () => { + const updated = await api.post( + `/store/carts/${cart.id}`, + { customer_id: customer.id }, + storeHeadersWithCustomer + ) + + expect(updated.status).toEqual(200) + expect(updated.data.cart).toEqual( + expect.objectContaining({ + email: customer.email, + customer: expect.objectContaining({ + id: customer.id, + email: customer.email, + }), + }) + ) + }) }) }) }, diff --git a/integration-tests/modules/helpers/create-authenticated-customer.ts b/integration-tests/modules/helpers/create-authenticated-customer.ts index bbf1c40f2d..eb1f7ed55b 100644 --- a/integration-tests/modules/helpers/create-authenticated-customer.ts +++ b/integration-tests/modules/helpers/create-authenticated-customer.ts @@ -36,7 +36,7 @@ export const createAuthenticatedCustomer = async ( actor_type: "customer", auth_identity_id: authIdentity.id, }, - http.jwtSecret + http.jwtSecret! ) return { customer, authIdentity, jwt: token } diff --git a/packages/core/core-flows/src/cart/workflows/index.ts b/packages/core/core-flows/src/cart/workflows/index.ts index 8bd5203d9e..ab7f3521e3 100644 --- a/packages/core/core-flows/src/cart/workflows/index.ts +++ b/packages/core/core-flows/src/cart/workflows/index.ts @@ -8,5 +8,6 @@ export * from "./list-shipping-options-for-cart" export * from "./refresh-payment-collection" export * from "./update-cart" export * from "./update-cart-promotions" +export * from "./update-cart-with-customer-validation" export * from "./update-line-item-in-cart" export * from "./update-tax-lines" diff --git a/packages/core/core-flows/src/cart/workflows/update-cart-with-customer-validation.ts b/packages/core/core-flows/src/cart/workflows/update-cart-with-customer-validation.ts new file mode 100644 index 0000000000..9b14d24330 --- /dev/null +++ b/packages/core/core-flows/src/cart/workflows/update-cart-with-customer-validation.ts @@ -0,0 +1,90 @@ +import { + AdditionalData, + UpdateCartWorkflowInputDTO, +} from "@medusajs/framework/types" +import { isDefined, isPresent, MedusaError } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { updateCartWorkflow } from "./update-cart" + +/** + * This step validates rules of engagement when customer_id or email is + * requested to be updated. + */ +export const validateCartCustomerOrEmailStep = createStep( + "validate-cart-customer-or-email", + async function ({ + input, + cart, + }: { + input: { + customer_id?: string | null + email?: string | null + auth_customer_id: string | undefined | null + } + cart: { customer_id: string | null; email: string | null } + }) { + if (isPresent(cart.customer_id) && cart.customer_id !== input.customer_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot update cart customer when customer_id is set` + ) + } + + if (isDefined(input.customer_id) && !isDefined(input.auth_customer_id)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `auth_customer_id is required when customer_id is set` + ) + } + + const isInputCustomerIdDifferent = + input.auth_customer_id !== input.customer_id + + if (isDefined(input.customer_id) && isInputCustomerIdDifferent) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot update cart customer_id to a different customer` + ) + } + } +) + +export const updateCartWorkflowWithCustomerValidationId = + "update-cart-with-customer-validation" +/** + * This workflow wraps updateCartWorkflow with customer validations + */ +export const updateCartWorkflowWithCustomerValidation = createWorkflow( + updateCartWorkflowWithCustomerValidationId, + ( + input: WorkflowData< + UpdateCartWorkflowInputDTO & + AdditionalData & { auth_customer_id: string | undefined } + > + ) => { + const cart = useRemoteQueryStep({ + entry_point: "cart", + variables: { id: input.id }, + fields: ["id", "customer_id", "email"], + list: false, + throw_if_key_not_found: true, + }).config({ name: "get-cart" }) + + when({ input }, ({ input }) => { + return !!input.customer_id || !!input.email + }).then(() => { + validateCartCustomerOrEmailStep({ input, cart }) + }) + + const updatedCart = updateCartWorkflow.runAsStep({ input }) + + return new WorkflowResponse(updatedCart) + } +) diff --git a/packages/medusa/src/api/store/carts/[id]/route.ts b/packages/medusa/src/api/store/carts/[id]/route.ts index 1a752e21f4..6de4b5c1ee 100644 --- a/packages/medusa/src/api/store/carts/[id]/route.ts +++ b/packages/medusa/src/api/store/carts/[id]/route.ts @@ -1,11 +1,15 @@ -import { updateCartWorkflow } from "@medusajs/core-flows" +import { updateCartWorkflowWithCustomerValidation } from "@medusajs/core-flows" import { AdditionalData, HttpTypes, UpdateCartDataDTO, } from "@medusajs/framework/types" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { + AuthenticatedMedusaRequest, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" import { refetchCart } from "../helpers" export const GET = async ( @@ -22,17 +26,15 @@ export const GET = async ( } export const POST = async ( - req: MedusaRequest, - res: MedusaResponse<{ - cart: HttpTypes.StoreCart - }> + req: AuthenticatedMedusaRequest, + res: MedusaResponse ) => { - const workflow = updateCartWorkflow(req.scope) - + const workflow = updateCartWorkflowWithCustomerValidation(req.scope) await workflow.run({ input: { ...req.validatedBody, id: req.params.id, + auth_customer_id: req.auth_context?.actor_id, }, }) diff --git a/packages/medusa/src/api/store/carts/validators.ts b/packages/medusa/src/api/store/carts/validators.ts index ee83af9a5a..2111cf60c0 100644 --- a/packages/medusa/src/api/store/carts/validators.ts +++ b/packages/medusa/src/api/store/carts/validators.ts @@ -46,6 +46,7 @@ export const StoreRemoveCartPromotions = z export type StoreUpdateCartType = z.infer export const UpdateCart = z .object({ + customer_id: z.string().optional(), region_id: z.string().optional(), email: z.string().email().nullish(), billing_address: z.union([AddressPayload, z.string()]).optional(),