feat(core-flows,medusa): Add customer validation on cart update (#9662)
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
import {
|
import {
|
||||||
Modules,
|
Modules,
|
||||||
PriceListStatus,
|
PriceListStatus,
|
||||||
@@ -6,39 +7,19 @@ import {
|
|||||||
PromotionRuleOperator,
|
PromotionRuleOperator,
|
||||||
PromotionType,
|
PromotionType,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
|
||||||
import {
|
import {
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
generatePublishableKey,
|
generatePublishableKey,
|
||||||
generateStoreHeaders,
|
generateStoreHeaders,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||||
|
import { createAuthenticatedCustomer } from "../../../../modules/helpers/create-authenticated-customer"
|
||||||
|
|
||||||
jest.setTimeout(100000)
|
jest.setTimeout(100000)
|
||||||
|
|
||||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||||
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
|
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 = {
|
const shippingAddressData = {
|
||||||
address_1: "test address 1",
|
address_1: "test address 1",
|
||||||
address_2: "test address 2",
|
address_2: "test address 2",
|
||||||
@@ -136,23 +117,20 @@ medusaIntegrationTestRunner({
|
|||||||
const publishableKey = await generatePublishableKey(appContainer)
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
storeHeaders = generateStoreHeaders({ publishableKey })
|
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
customer = (
|
const result = await createAuthenticatedCustomer(appContainer, {
|
||||||
await api.post(
|
first_name: "tony",
|
||||||
"/admin/customers",
|
last_name: "stark",
|
||||||
{
|
email: "tony@stark-industries.com",
|
||||||
first_name: "tony",
|
|
||||||
email: "tony@stark-industries.com",
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.customer
|
|
||||||
|
|
||||||
storeHeadersWithCustomer = await generateStoreHeadersWithCustomer({
|
|
||||||
storeHeaders,
|
|
||||||
api,
|
|
||||||
customer,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
customer = result.customer
|
||||||
|
storeHeadersWithCustomer = {
|
||||||
|
headers: {
|
||||||
|
...storeHeaders.headers,
|
||||||
|
authorization: `Bearer ${result.jwt}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
await setupTaxStructure(appContainer.resolve(Modules.TAX))
|
await setupTaxStructure(appContainer.resolve(Modules.TAX))
|
||||||
|
|
||||||
region = (
|
region = (
|
||||||
@@ -579,23 +557,23 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /store/carts/:id", () => {
|
describe("POST /store/carts/:id", () => {
|
||||||
let otherRegion
|
let otherRegion, cartWithCustomer
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
cart = (
|
const cartData = {
|
||||||
await api.post(
|
currency_code: "usd",
|
||||||
`/store/carts`,
|
sales_channel_id: salesChannel.id,
|
||||||
{
|
region_id: region.id,
|
||||||
email: "tony@stark.com",
|
shipping_address: shippingAddressData,
|
||||||
currency_code: "usd",
|
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||||
sales_channel_id: salesChannel.id,
|
promo_codes: [promotion.code],
|
||||||
region_id: region.id,
|
}
|
||||||
shipping_address: shippingAddressData,
|
|
||||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
cart = (await api.post(`/store/carts`, cartData, storeHeaders)).data
|
||||||
promo_codes: [promotion.code],
|
.cart
|
||||||
},
|
|
||||||
storeHeadersWithCustomer
|
cartWithCustomer = (
|
||||||
)
|
await api.post(`/store/carts`, cartData, storeHeadersWithCustomer)
|
||||||
).data.cart
|
).data.cart
|
||||||
|
|
||||||
otherRegion = (
|
otherRegion = (
|
||||||
@@ -751,7 +729,7 @@ medusaIntegrationTestRunner({
|
|||||||
it("should not generate tax lines if automatic taxes is false", async () => {
|
it("should not generate tax lines if automatic taxes is false", async () => {
|
||||||
let updated = await api.post(
|
let updated = await api.post(
|
||||||
`/store/carts/${cart.id}`,
|
`/store/carts/${cart.id}`,
|
||||||
{ email: "another@tax.com" },
|
{},
|
||||||
storeHeaders
|
storeHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -776,7 +754,7 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
updated = await api.post(
|
updated = await api.post(
|
||||||
`/store/carts/${cart.id}`,
|
`/store/carts/${cart.id}`,
|
||||||
{ email: "another@tax.com", region_id: noAutomaticRegion.id },
|
{ region_id: noAutomaticRegion.id },
|
||||||
storeHeaders
|
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,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const createAuthenticatedCustomer = async (
|
|||||||
actor_type: "customer",
|
actor_type: "customer",
|
||||||
auth_identity_id: authIdentity.id,
|
auth_identity_id: authIdentity.id,
|
||||||
},
|
},
|
||||||
http.jwtSecret
|
http.jwtSecret!
|
||||||
)
|
)
|
||||||
|
|
||||||
return { customer, authIdentity, jwt: token }
|
return { customer, authIdentity, jwt: token }
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ export * from "./list-shipping-options-for-cart"
|
|||||||
export * from "./refresh-payment-collection"
|
export * from "./refresh-payment-collection"
|
||||||
export * from "./update-cart"
|
export * from "./update-cart"
|
||||||
export * from "./update-cart-promotions"
|
export * from "./update-cart-promotions"
|
||||||
|
export * from "./update-cart-with-customer-validation"
|
||||||
export * from "./update-line-item-in-cart"
|
export * from "./update-line-item-in-cart"
|
||||||
export * from "./update-tax-lines"
|
export * from "./update-tax-lines"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { updateCartWorkflow } from "@medusajs/core-flows"
|
import { updateCartWorkflowWithCustomerValidation } from "@medusajs/core-flows"
|
||||||
import {
|
import {
|
||||||
AdditionalData,
|
AdditionalData,
|
||||||
HttpTypes,
|
HttpTypes,
|
||||||
UpdateCartDataDTO,
|
UpdateCartDataDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
|
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "@medusajs/framework/http"
|
||||||
import { refetchCart } from "../helpers"
|
import { refetchCart } from "../helpers"
|
||||||
|
|
||||||
export const GET = async (
|
export const GET = async (
|
||||||
@@ -22,17 +26,15 @@ export const GET = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: MedusaRequest<UpdateCartDataDTO & AdditionalData>,
|
req: AuthenticatedMedusaRequest<UpdateCartDataDTO & AdditionalData>,
|
||||||
res: MedusaResponse<{
|
res: MedusaResponse<HttpTypes.StoreCartResponse>
|
||||||
cart: HttpTypes.StoreCart
|
|
||||||
}>
|
|
||||||
) => {
|
) => {
|
||||||
const workflow = updateCartWorkflow(req.scope)
|
const workflow = updateCartWorkflowWithCustomerValidation(req.scope)
|
||||||
|
|
||||||
await workflow.run({
|
await workflow.run({
|
||||||
input: {
|
input: {
|
||||||
...req.validatedBody,
|
...req.validatedBody,
|
||||||
id: req.params.id,
|
id: req.params.id,
|
||||||
|
auth_customer_id: req.auth_context?.actor_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const StoreRemoveCartPromotions = z
|
|||||||
export type StoreUpdateCartType = z.infer<typeof UpdateCart>
|
export type StoreUpdateCartType = z.infer<typeof UpdateCart>
|
||||||
export const UpdateCart = z
|
export const UpdateCart = z
|
||||||
.object({
|
.object({
|
||||||
|
customer_id: z.string().optional(),
|
||||||
region_id: z.string().optional(),
|
region_id: z.string().optional(),
|
||||||
email: z.string().email().nullish(),
|
email: z.string().email().nullish(),
|
||||||
billing_address: z.union([AddressPayload, z.string()]).optional(),
|
billing_address: z.union([AddressPayload, z.string()]).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user