feat(core-flows): validate hook in cart workflows (#10967)

* feat(core-flows): validate hook

* rm only
This commit is contained in:
Carlos R. L. Rodrigues
2025-01-15 20:49:46 -03:00
committed by GitHub
parent 2a25b4d95f
commit 11f98f374c
14 changed files with 194 additions and 74 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
feat(core-flows): validation hook on cart mutations

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<AddShippingMethodToCartWorkflowInput>
): WorkflowData<void> => {
(input: WorkflowData<AddShippingMethodToCartWorkflowInput>) => {
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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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<CompleteCartWorkflowInput>
): WorkflowResponse<{ id: string }> => {
(input: WorkflowData<CompleteCartWorkflowInput>) => {
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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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<RefreshPaymentCollectionForCartWorklowInput>
): WorkflowData<void> => {
(input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>) => {
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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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<UpdateCartPromotionsWorkflowInput>
): WorkflowData<void> => {
(input: WorkflowData<UpdateCartPromotionsWorkflowInput>) => {
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],
})
}
)

View File

@@ -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],
})
}
)

View File

@@ -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],
})
}
)