Skip to content

Commit

Permalink
feat(core-flows,medusa): Add customer validation on cart update (#9662)
Browse files Browse the repository at this point in the history
  • Loading branch information
riqwan authored Oct 22, 2024
1 parent 68cb7d0 commit 0d803b3
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 62 deletions.
181 changes: 128 additions & 53 deletions integration-tests/http/__tests__/cart/store/cart.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
Modules,
PriceListStatus,
Expand All @@ -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",
Expand Down Expand Up @@ -136,23 +117,20 @@ medusaIntegrationTestRunner({
const publishableKey = await generatePublishableKey(appContainer)
storeHeaders = generateStoreHeaders({ publishableKey })

customer = (
await api.post(
"/admin/customers",
{
first_name: "tony",
email: "[email protected]",
},
adminHeaders
)
).data.customer

storeHeadersWithCustomer = await generateStoreHeadersWithCustomer({
storeHeaders,
api,
customer,
const result = await createAuthenticatedCustomer(appContainer, {
first_name: "tony",
last_name: "stark",
email: "[email protected]",
})

customer = result.customer
storeHeadersWithCustomer = {
headers: {
...storeHeaders.headers,
authorization: `Bearer ${result.jwt}`,
},
}

await setupTaxStructure(appContainer.resolve(Modules.TAX))

region = (
Expand Down Expand Up @@ -579,23 +557,23 @@ medusaIntegrationTestRunner({
})

describe("POST /store/carts/:id", () => {
let otherRegion
let otherRegion, cartWithCustomer

beforeEach(async () => {
cart = (
await api.post(
`/store/carts`,
{
email: "[email protected]",
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 = (
Expand Down Expand Up @@ -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: "[email protected]" },
{},
storeHeaders
)

Expand All @@ -776,7 +754,7 @@ medusaIntegrationTestRunner({

updated = await api.post(
`/store/carts/${cart.id}`,
{ email: "[email protected]", region_id: noAutomaticRegion.id },
{ region_id: noAutomaticRegion.id },
storeHeaders
)

Expand Down Expand Up @@ -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: "[email protected]" },
storeHeaders
)

expect(updated.status).toEqual(200)
expect(updated.data.cart).toEqual(
expect.objectContaining({
email: "[email protected]",
customer: expect.objectContaining({
email: "[email protected]",
}),
})
)
})

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: "[email protected]",
},
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,
}),
})
)
})
})
})
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions packages/core/core-flows/src/cart/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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)
}
)
18 changes: 10 additions & 8 deletions packages/medusa/src/api/store/carts/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -22,17 +26,15 @@ export const GET = async (
}

export const POST = async (
req: MedusaRequest<UpdateCartDataDTO & AdditionalData>,
res: MedusaResponse<{
cart: HttpTypes.StoreCart
}>
req: AuthenticatedMedusaRequest<UpdateCartDataDTO & AdditionalData>,
res: MedusaResponse<HttpTypes.StoreCartResponse>
) => {
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,
},
})

Expand Down
1 change: 1 addition & 0 deletions packages/medusa/src/api/store/carts/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const StoreRemoveCartPromotions = z
export type StoreUpdateCartType = z.infer<typeof UpdateCart>
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(),
Expand Down

0 comments on commit 0d803b3

Please sign in to comment.