From 11f98f374cb0b77b1c80e72328fc68b5e22d5b87 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Wed, 15 Jan 2025 20:49:46 -0300 Subject: [PATCH] feat(core-flows): validate hook in cart workflows (#10967) * feat(core-flows): validate hook * rm only --- .changeset/quiet-beers-compare.md | 5 ++ .../cart/store/cart.workflows.spec.ts | 65 ++++++++++++------- .../src/cart/steps/validate-cart.ts | 9 +-- .../workflows/add-shipping-method-to-cart.ts | 16 ++++- .../src/cart/workflows/add-to-cart.ts | 10 +++ .../src/cart/workflows/complete-cart.ts | 28 ++++---- .../src/cart/workflows/create-carts.ts | 25 ++++--- .../src/cart/workflows/refresh-cart-items.ts | 10 ++- .../refresh-cart-shipping-methods.ts | 11 ++++ .../workflows/refresh-payment-collection.ts | 15 ++++- .../cart/workflows/transfer-cart-customer.ts | 11 ++++ .../cart/workflows/update-cart-promotions.ts | 15 ++++- .../src/cart/workflows/update-cart.ts | 33 ++++++---- .../workflows/update-line-item-in-cart.ts | 15 ++++- 14 files changed, 194 insertions(+), 74 deletions(-) create mode 100644 .changeset/quiet-beers-compare.md diff --git a/.changeset/quiet-beers-compare.md b/.changeset/quiet-beers-compare.md new file mode 100644 index 0000000000..16e089a669 --- /dev/null +++ b/.changeset/quiet-beers-compare.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +feat(core-flows): validation hook on cart mutations diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 616839d755..9ee3d811de 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -845,6 +845,11 @@ medusaIntegrationTestRunner({ describe("UpdateCartWorkflow", () => { it("should remove item with custom price when region is updated", async () => { + const hookCallback = jest.fn() + addToCartWorkflow.hooks.validate((data) => { + hookCallback(data) + }) + const salesChannel = await scModuleService.createSalesChannels({ name: "Webshop", }) @@ -926,35 +931,51 @@ medusaIntegrationTestRunner({ select: ["id", "region_id", "currency_code", "sales_channel_id"], }) + const wfInput = { + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + { + title: "Test item", + subtitle: "Test subtitle", + thumbnail: "some-url", + requires_shipping: true, + is_discountable: false, + is_tax_inclusive: false, + unit_price: 1500, + metadata: { + foo: "bar", + }, + quantity: 1, + }, + ], + cart_id: cart.id, + } await addToCartWorkflow(appContainer).run({ - input: { - items: [ - { - variant_id: product.variants[0].id, - quantity: 1, - }, - { - title: "Test item", - subtitle: "Test subtitle", - thumbnail: "some-url", - requires_shipping: true, - is_discountable: false, - is_tax_inclusive: false, - unit_price: 1500, - metadata: { - foo: "bar", - }, - quantity: 1, - }, - ], - cart_id: cart.id, - }, + input: wfInput, }) cart = await cartModuleService.retrieveCart(cart.id, { relations: ["items"], }) + expect(hookCallback).toHaveBeenCalledWith({ + cart: { + completed_at: null, + id: expect.stringContaining("cart_"), + sales_channel_id: expect.stringContaining("sc_"), + currency_code: "usd", + region_id: expect.stringContaining("reg_"), + item_total: 0, + total: 0, + email: null, + customer_id: null, + }, + input: wfInput, + }) + expect(cart).toEqual( expect.objectContaining({ id: cart.id, diff --git a/packages/core/core-flows/src/cart/steps/validate-cart.ts b/packages/core/core-flows/src/cart/steps/validate-cart.ts index 646dc72849..3487d6dab7 100644 --- a/packages/core/core-flows/src/cart/steps/validate-cart.ts +++ b/packages/core/core-flows/src/cart/steps/validate-cart.ts @@ -1,5 +1,5 @@ import { CartDTO, CartWorkflowDTO } from "@medusajs/framework/types" -import { MedusaError, isPresent } from "@medusajs/framework/utils" +import { MedusaError } from "@medusajs/framework/utils" import { createStep } from "@medusajs/framework/workflows-sdk" export interface ValidateCartStepInput { @@ -15,13 +15,6 @@ export const validateCartStep = createStep( async (data: ValidateCartStepInput) => { const { cart } = data - if (!isPresent(cart)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Cart does not exist` - ) - } - if (cart.completed_at) { throw new MedusaError( MedusaError.Types.INVALID_DATA, diff --git a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts index b5a5e842e6..275afd398a 100644 --- a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts @@ -1,9 +1,11 @@ import { CartWorkflowEvents, MedusaError } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" @@ -33,18 +35,22 @@ export const addShippingMethodToCartWorkflowId = "add-shipping-method-to-cart" */ export const addShippingMethodToCartWorkflow = createWorkflow( addShippingMethodToCartWorkflowId, - ( - input: WorkflowData - ): WorkflowData => { + (input: WorkflowData) => { const cart = useRemoteQueryStep({ entry_point: "cart", fields: cartFieldsForRefreshSteps, variables: { id: input.cart_id }, list: false, + throw_if_key_not_found: true, }) validateCartStep({ cart }) + const validate = createHook("validate", { + input, + cart, + }) + const optionIds = transform({ input }, (data) => { return (data.input.options ?? []).map((i) => i.id) }) @@ -151,5 +157,9 @@ export const addShippingMethodToCartWorkflow = createWorkflow( refreshCartItemsWorkflow.runAsStep({ input: { cart_id: cart.id }, }) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts index 737305b5a9..303f0ba8b4 100644 --- a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts @@ -1,11 +1,13 @@ import { AddToCartWorkflowInputDTO } from "@medusajs/framework/types" import { CartWorkflowEvents, isDefined } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, when, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep } from "../../common" import { emitEventStep } from "../../common/steps/emit-event" @@ -50,6 +52,10 @@ export const addToCartWorkflow = createWorkflow( }) validateCartStep({ cart }) + const validate = createHook("validate", { + input, + cart, + }) const variantIds = transform({ input }, (data) => { return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean) @@ -134,5 +140,9 @@ export const addToCartWorkflow = createWorkflow( eventName: CartWorkflowEvents.UPDATED, data: { id: cart.id }, }) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/complete-cart.ts b/packages/core/core-flows/src/cart/workflows/complete-cart.ts index 01096ed328..e5469360da 100644 --- a/packages/core/core-flows/src/cart/workflows/complete-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/complete-cart.ts @@ -8,6 +8,7 @@ import { OrderWorkflowEvents, } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, @@ -52,9 +53,7 @@ export const completeCartWorkflow = createWorkflow( idempotent: true, retentionTime: THREE_DAYS, }, - ( - input: WorkflowData - ): WorkflowResponse<{ id: string }> => { + (input: WorkflowData) => { const orderCart = useQueryGraphStep({ entity: "order_cart", fields: ["cart_id", "order_id"], @@ -65,17 +64,22 @@ export const completeCartWorkflow = createWorkflow( return orderCart.data[0]?.order_id }) + const cart = useRemoteQueryStep({ + entry_point: "cart", + fields: completeCartFields, + variables: { id: input.id }, + list: false, + }) + + const validate = createHook("validate", { + input, + cart, + }) + // If order ID does not exist, we are completing the cart for the first time const order = when("create-order", { orderId }, ({ orderId }) => { return !orderId }).then(() => { - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: completeCartFields, - variables: { id: input.id }, - list: false, - }) - const paymentSessions = validateCartPaymentsStep({ cart }) const payment = authorizePaymentSessionStep({ @@ -267,6 +271,8 @@ export const completeCartWorkflow = createWorkflow( return { id: order?.id ?? orderId } }) - return new WorkflowResponse(result) + return new WorkflowResponse(result, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/create-carts.ts b/packages/core/core-flows/src/cart/workflows/create-carts.ts index 1d35a33221..a83310906b 100644 --- a/packages/core/core-flows/src/cart/workflows/create-carts.ts +++ b/packages/core/core-flows/src/cart/workflows/create-carts.ts @@ -39,17 +39,18 @@ import { updateTaxLinesWorkflow } from "./update-tax-lines" /** * The data to create the cart, along with custom data that's passed to the workflow's hooks. */ -export type CreateCartWorkflowInput = CreateCartWorkflowInputDTO & AdditionalData +export type CreateCartWorkflowInput = CreateCartWorkflowInputDTO & + AdditionalData export const createCartWorkflowId = "create-cart" /** - * This workflow creates and returns a cart. You can set the cart's items, region, customer, and other details. This workflow is executed by the + * This workflow creates and returns a cart. You can set the cart's items, region, customer, and other details. This workflow is executed by the * [Create Cart Store API Route](https://docs.medusajs.com/api/store#carts_postcarts). - * + * * This workflow has a hook that allows you to perform custom actions on the created cart. You can see an example in [this guide](https://docs.medusajs.com/resources/commerce-modules/cart/extend#step-4-consume-cartcreated-workflow-hook). - * + * * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around cart creation. - * + * * @example * const { result } = await createCartWorkflow(container) * .run({ @@ -67,11 +68,12 @@ export const createCartWorkflowId = "create-cart" * } * } * }) - * + * * @summary - * + * * Create a cart specifying region, items, and more. - * + * + * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. * @property hooks.cartCreated - This hook is executed after a cart is created. You can consume this hook to perform custom actions on the created cart. */ export const createCartWorkflow = createWorkflow( @@ -206,6 +208,11 @@ export const createCartWorkflow = createWorkflow( } }) + const validate = createHook("validate", { + input: cartInput, + cart: cartToCreate, + }) + const carts = createCartsStep([cartToCreate]) const cart = transform({ carts }, (data) => data.carts?.[0]) @@ -240,7 +247,7 @@ export const createCartWorkflow = createWorkflow( }) return new WorkflowResponse(cart, { - hooks: [cartCreated], + hooks: [validate, cartCreated], }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts index 722cd2a625..6a6096bf0c 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts @@ -4,6 +4,7 @@ import { PromotionActions, } from "@medusajs/framework/utils" import { + createHook, createWorkflow, transform, when, @@ -71,6 +72,11 @@ export const refreshCartItemsWorkflow = createWorkflow( validateVariantPricesStep({ variants }) + const validate = createHook("validate", { + input, + cart, + }) + const lineItems = transform({ cart, variants }, ({ cart, variants }) => { const items = cart.items.map((item) => { const variant = (variants ?? []).find((v) => v.id === item.variant_id)! @@ -143,6 +149,8 @@ export const refreshCartItemsWorkflow = createWorkflow( input: { cart_id: cart.id }, }) - return new WorkflowResponse(refetchedCart) + return new WorkflowResponse(refetchedCart, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts index 60d47f22d1..77bfaaf163 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts @@ -1,10 +1,12 @@ import { isDefined, isPresent } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, when, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep } from "../../common" import { removeShippingMethodFromCartStep } from "../steps" @@ -48,6 +50,11 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow( .filter(Boolean) ) + const validate = createHook("validate", { + input, + cart, + }) + when({ listShippingOptionsInput }, ({ listShippingOptionsInput }) => { return !!listShippingOptionsInput?.length }).then(() => { @@ -126,5 +133,9 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow( updateShippingMethodsStep(shippingMethodsData.shippingMethodsToUpdate) ) }) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts b/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts index 8d82548326..cdcbd28b1e 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-payment-collection.ts @@ -1,6 +1,8 @@ import { MathBN, isPresent } from "@medusajs/framework/utils" import { WorkflowData, + WorkflowResponse, + createHook, createWorkflow, parallelize, transform, @@ -21,9 +23,7 @@ export const refreshPaymentCollectionForCartWorkflowId = */ export const refreshPaymentCollectionForCartWorkflow = createWorkflow( refreshPaymentCollectionForCartWorkflowId, - ( - input: WorkflowData - ): WorkflowData => { + (input: WorkflowData) => { const cart = useRemoteQueryStep({ entry_point: "cart", fields: [ @@ -43,6 +43,11 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow( list: false, }) + const validate = createHook("validate", { + input, + cart, + }) + when({ cart }, ({ cart }) => { const valueIsEqual = MathBN.eq( cart.payment_collection?.raw_amount ?? -1, @@ -89,5 +94,9 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow( updatePaymentCollectionStep(updatePaymentCollectionInput) ) }) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/transfer-cart-customer.ts b/packages/core/core-flows/src/cart/workflows/transfer-cart-customer.ts index 152c665cd7..48ccabfdba 100644 --- a/packages/core/core-flows/src/cart/workflows/transfer-cart-customer.ts +++ b/packages/core/core-flows/src/cart/workflows/transfer-cart-customer.ts @@ -1,8 +1,10 @@ import { + createHook, createWorkflow, transform, when, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep } from "../../common" import { updateCartsStep } from "../steps" @@ -32,6 +34,11 @@ export const transferCartCustomerWorkflow = createWorkflow( const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0]) + const validate = createHook("validate", { + input, + cart, + }) + const customerQuery = useQueryGraphStep({ entity: "customer", filters: { id: input.customer_id }, @@ -72,5 +79,9 @@ export const transferCartCustomerWorkflow = createWorkflow( }) } ) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/update-cart-promotions.ts b/packages/core/core-flows/src/cart/workflows/update-cart-promotions.ts index 63afb6ae70..868bd91702 100644 --- a/packages/core/core-flows/src/cart/workflows/update-cart-promotions.ts +++ b/packages/core/core-flows/src/cart/workflows/update-cart-promotions.ts @@ -1,9 +1,11 @@ import { PromotionActions } from "@medusajs/framework/utils" import { + createHook, createWorkflow, parallelize, transform, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useRemoteQueryStep } from "../../common" import { @@ -33,9 +35,7 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions" */ export const updateCartPromotionsWorkflow = createWorkflow( updateCartPromotionsWorkflowId, - ( - input: WorkflowData - ): WorkflowData => { + (input: WorkflowData) => { const cart = useRemoteQueryStep({ entry_point: "cart", fields: cartFieldsForRefreshSteps, @@ -43,6 +43,11 @@ export const updateCartPromotionsWorkflow = createWorkflow( list: false, }) + const validate = createHook("validate", { + input, + cart, + }) + const promo_codes = transform({ input }, (data) => { return (data.input.promo_codes || []) as string[] }) @@ -85,5 +90,9 @@ export const updateCartPromotionsWorkflow = createWorkflow( action: PromotionActions.REPLACE, }) ) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/update-cart.ts b/packages/core/core-flows/src/cart/workflows/update-cart.ts index 325697bfee..baeea6ce19 100644 --- a/packages/core/core-flows/src/cart/workflows/update-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-cart.ts @@ -32,24 +32,25 @@ import { refreshCartItemsWorkflow } from "./refresh-cart-items" /** * The data to update the cart, along with custom data that's passed to the workflow's hooks. */ -export type UpdateCartWorkflowInput = UpdateCartWorkflowInputDTO & AdditionalData +export type UpdateCartWorkflowInput = UpdateCartWorkflowInputDTO & + AdditionalData export const updateCartWorkflowId = "update-cart" /** - * This workflow updates a cart and returns it. You can update the cart's region, address, and more. This workflow is executed by the + * This workflow updates a cart and returns it. You can update the cart's region, address, and more. This workflow is executed by the * [Update Cart Store API Route](https://docs.medusajs.com/api/store#carts_postcartsid). - * + * * :::note - * + * * This workflow doesn't allow updating a cart's line items. Instead, use {@link addToCartWorkflow} and {@link updateLineItemInCartWorkflow}. - * + * * ::: - * - * This workflow has a hook that allows you to perform custom actions on the updated cart. For example, you can pass custom data under the `additional_data` property of the Update Cart API route, + * + * This workflow has a hook that allows you to perform custom actions on the updated cart. For example, you can pass custom data under the `additional_data` property of the Update Cart API route, * then update any associated details related to the cart in the workflow's hook. - * + * * You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around updating a cart. - * + * * @example * const { result } = await updateCartWorkflow(container) * .run({ @@ -70,11 +71,12 @@ export const updateCartWorkflowId = "update-cart" * } * } * }) - * + * * @summary - * + * * Update a cart's details, such as region, address, and more. - * + * + * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. * @property hooks.cartUpdated - This hook is executed after a cart is update. You can consume this hook to perform custom actions on the updated cart. */ export const updateCartWorkflow = createWorkflow( @@ -205,6 +207,11 @@ export const updateCartWorkflow = createWorkflow( } ) + const validate = createHook("validate", { + input: cartInput, + cart: cartToUpdate, + }) + /* when({ cartInput }, ({ cartInput }) => { return isDefined(cartInput.customer_id) || isDefined(cartInput.email) @@ -274,7 +281,7 @@ export const updateCartWorkflow = createWorkflow( }) return new WorkflowResponse(void 0, { - hooks: [cartUpdated], + hooks: [validate, cartUpdated], }) } ) diff --git a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts index 3243a198b1..287500ea3a 100644 --- a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts @@ -1,10 +1,12 @@ import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types" import { isDefined, MedusaError } from "@medusajs/framework/utils" import { + createHook, createWorkflow, transform, when, WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep } from "../../common" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" @@ -41,6 +43,11 @@ export const updateLineItemInCartWorkflow = createWorkflow( validateCartStep({ cart }) + const validate = createHook("validate", { + input, + cart, + }) + const variantIds = transform({ item }, ({ item }) => { return [item.variant_id].filter(Boolean) }) @@ -63,7 +70,9 @@ export const updateLineItemInCartWorkflow = createWorkflow( validateVariantPricesStep({ variants }) const items = transform({ input, item }, (data) => { - return [Object.assign(data.item, { quantity: data.input.update.quantity })] + return [ + Object.assign(data.item, { quantity: data.input.update.quantity }), + ] }) confirmVariantInventoryWorkflow.runAsStep({ @@ -115,5 +124,9 @@ export const updateLineItemInCartWorkflow = createWorkflow( refreshCartItemsWorkflow.runAsStep({ input: { cart_id: input.cart_id }, }) + + return new WorkflowResponse(void 0, { + hooks: [validate], + }) } )