From fa1793e8e92164251c776859dea7a5e312ef9432 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:00:04 -0300 Subject: [PATCH] chore(core-utils): avoid overfetching to refresh cart (#11602) What: * Not all Cart operations need a full refresh updating items. This PR introduces a flag to force the refresh for special ocasions, like updating the Cart's region, or transfering the Cart to another customer. For all other flows it will update only promotions, taxes and payment collection if needed. --- .changeset/twenty-oranges-agree.md | 5 + .../cart/store/cart.workflows.spec.ts | 2 +- .../core/core-flows/src/cart/utils/fields.ts | 5 + .../src/cart/workflows/refresh-cart-items.ts | 133 +++++++++--------- .../workflows/refresh-payment-collection.ts | 62 ++++---- .../cart/workflows/transfer-cart-customer.ts | 16 +-- .../src/cart/workflows/update-cart.ts | 8 +- 7 files changed, 126 insertions(+), 105 deletions(-) create mode 100644 .changeset/twenty-oranges-agree.md diff --git a/.changeset/twenty-oranges-agree.md b/.changeset/twenty-oranges-agree.md new file mode 100644 index 0000000000..31478572de --- /dev/null +++ b/.changeset/twenty-oranges-agree.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +chore(core-flows): avoid overfetching to refresh cart 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 ff4a908eaf..7d53b67c86 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -2068,7 +2068,7 @@ medusaIntegrationTestRunner({ unit_price: 4000, is_custom_price: true, quantity: 2, - title: "Test variant", + title: "Test item", }) ) }) diff --git a/packages/core/core-flows/src/cart/utils/fields.ts b/packages/core/core-flows/src/cart/utils/fields.ts index f7bf8def30..8dd6f25357 100644 --- a/packages/core/core-flows/src/cart/utils/fields.ts +++ b/packages/core/core-flows/src/cart/utils/fields.ts @@ -35,6 +35,11 @@ export const cartFieldsForRefreshSteps = [ "customer.*", "customer.groups.*", "promotions.code", + "payment_collection.id", + "payment_collection.raw_amount", + "payment_collection.amount", + "payment_collection.currency_code", + "payment_collection.payment_sessions.id", ] export const completeCartFields = [ 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 1a8ad28842..807dea450d 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,7 +4,6 @@ import { PromotionActions, } from "@medusajs/framework/utils" import { - createHook, createWorkflow, transform, when, @@ -41,19 +40,23 @@ export type RefreshCartItemsWorkflowInput = { * These promotion codes will replace previously applied codes. */ promo_codes?: string[] + /** + * Force refresh the cart items + */ + force_refresh?: boolean } export const refreshCartItemsWorkflowId = "refresh-cart-items" /** * This workflow refreshes a cart to ensure its prices, promotion codes, taxes, and other details are applied correctly. It's useful * after making a chnge to a cart, such as after adding an item to the cart or adding a promotion code. - * + * * This workflow is used by other cart-related workflows, such as the {@link addToCartWorkflow} after an item * is added to the cart. - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to refresh the cart after making updates to it in your * custom flows. - * + * * @example * const { result } = await refreshCartItemsWorkflow(container) * .run({ @@ -61,37 +64,34 @@ export const refreshCartItemsWorkflowId = "refresh-cart-items" * cart_id: "cart_123", * } * }) - * + * * @summary - * + * * Refresh a cart's details after an update. - * - * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution. + * */ export const refreshCartItemsWorkflow = createWorkflow( refreshCartItemsWorkflowId, - ( - input: WorkflowData - ) => { - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: cartFieldsForRefreshSteps, - variables: { id: input.cart_id }, - list: false, - }) - - const variantIds = transform({ cart }, (data) => { - return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean) - }) - - const cartPricingContext = transform({ cart }, ({ cart }) => { - return filterObjectByKeys(cart, cartFieldsForPricingContext) - }) - - const variants = when({ variantIds }, ({ variantIds }) => { - return !!variantIds.length + (input: WorkflowData) => { + when({ input }, ({ input }) => { + return !!input.force_refresh }).then(() => { - return useRemoteQueryStep({ + const cart = useRemoteQueryStep({ + entry_point: "cart", + fields: cartFieldsForRefreshSteps, + variables: { id: input.cart_id }, + list: false, + }) + + const variantIds = transform({ cart }, (data) => { + return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean) + }) + + const cartPricingContext = transform({ cart }, ({ cart }) => { + return filterObjectByKeys(cart, cartFieldsForPricingContext) + }) + + const variants = useRemoteQueryStep({ entry_point: "variants", fields: productVariantsFields, variables: { @@ -101,62 +101,59 @@ export const refreshCartItemsWorkflow = createWorkflow( }, }, }).config({ name: "fetch-variants" }) - }) - validateVariantPricesStep({ variants }) + 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 + )! - const lineItems = transform({ cart, variants }, ({ cart, variants }) => { - const items = cart.items.map((item) => { - const variant = (variants ?? []).find((v) => v.id === item.variant_id)! + const input: PrepareLineItemDataInput = { + item, + variant: variant, + cartId: cart.id, + unitPrice: item.unit_price, + isTaxInclusive: item.is_tax_inclusive, + } - const input: PrepareLineItemDataInput = { - item, - variant: variant, - cartId: cart.id, - unitPrice: item.unit_price, - isTaxInclusive: item.is_tax_inclusive, - } + if (variant && !item.is_custom_price) { + input.unitPrice = variant.calculated_price?.calculated_amount + input.isTaxInclusive = + variant.calculated_price?.is_calculated_price_tax_inclusive + } - if (variant && !item.is_custom_price) { - input.unitPrice = variant.calculated_price?.calculated_amount - input.isTaxInclusive = - variant.calculated_price?.is_calculated_price_tax_inclusive - } + const preparedItem = prepareLineItemData(input) - const preparedItem = prepareLineItemData(input) + return { + selector: { id: item.id }, + data: preparedItem, + } + }) - return { - selector: { id: item.id }, - data: preparedItem, - } + return items }) - return items - }) - - updateLineItemsStep({ - id: cart.id, - items: lineItems, + updateLineItemsStep({ + id: cart.id, + items: lineItems, + }) }) const refetchedCart = useRemoteQueryStep({ entry_point: "cart", fields: cartFieldsForRefreshSteps, - variables: { id: cart.id }, + variables: { id: input.cart_id }, list: false, }).config({ name: "refetch–cart" }) refreshCartShippingMethodsWorkflow.runAsStep({ - input: { cart_id: cart.id }, + input: { cart_id: input.cart_id }, }) updateTaxLinesWorkflow.runAsStep({ - input: { cart_id: cart.id }, + input: { cart_id: input.cart_id }, }) const cartPromoCodes = transform( @@ -172,18 +169,16 @@ export const refreshCartItemsWorkflow = createWorkflow( updateCartPromotionsWorkflow.runAsStep({ input: { - cart_id: cart.id, + cart_id: input.cart_id, promo_codes: cartPromoCodes, action: PromotionActions.REPLACE, }, }) refreshPaymentCollectionForCartWorkflow.runAsStep({ - input: { cart_id: cart.id }, + input: { cart: refetchedCart }, }) - return new WorkflowResponse(refetchedCart, { - hooks: [validate], - }) + return new WorkflowResponse(refetchedCart) } ) 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 e873df8202..6eb15c0295 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 @@ -19,22 +19,26 @@ export type RefreshPaymentCollectionForCartWorklowInput = { /** * The cart's ID. */ - cart_id: string + cart_id?: string + /** + * The Cart reference. + */ + cart?: any } export const refreshPaymentCollectionForCartWorkflowId = "refresh-payment-collection-for-cart" /** * This workflow refreshes a cart's payment collection, which is useful once the cart is created or when its details - * are updated. If the cart's total changes to the amount in its payment collection, the payment collection's payment sessions are + * are updated. If the cart's total changes to the amount in its payment collection, the payment collection's payment sessions are * deleted. It also syncs the payment collection's amount, currency code, and other details with the details in the cart. - * + * * This workflow is used by other cart-related workflows, such as the {@link refreshCartItemsWorkflow} to refresh the cart's * payment collection after an update. - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to refresh the cart's payment collection after making updates to it in your * custom flows. - * + * * @example * const { result } = await refreshPaymentCollectionForCartWorkflow(container) * .run({ @@ -42,33 +46,41 @@ export const refreshPaymentCollectionForCartWorkflowId = * cart_id: "cart_123", * } * }) - * + * * @summary - * + * * Refresh a cart's payment collection details. - * + * * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution. */ export const refreshPaymentCollectionForCartWorkflow = createWorkflow( refreshPaymentCollectionForCartWorkflowId, (input: WorkflowData) => { - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: [ - "id", - "region_id", - "currency_code", - "total", - "raw_total", - "payment_collection.id", - "payment_collection.raw_amount", - "payment_collection.amount", - "payment_collection.currency_code", - "payment_collection.payment_sessions.id", - ], - variables: { id: input.cart_id }, - throw_if_key_not_found: true, - list: false, + const fetchCart = when({ input }, ({ input }) => { + return !input.cart + }).then(() => { + return useRemoteQueryStep({ + entry_point: "cart", + fields: [ + "id", + "region_id", + "currency_code", + "total", + "raw_total", + "payment_collection.id", + "payment_collection.raw_amount", + "payment_collection.amount", + "payment_collection.currency_code", + "payment_collection.payment_sessions.id", + ], + variables: { id: input.cart_id }, + throw_if_key_not_found: true, + list: false, + }) + }) + + const cart = transform({ fetchCart, input }, ({ fetchCart, input }) => { + return input.cart ?? fetchCart }) const validate = createHook("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 afe1e24220..1b6765a2fc 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 @@ -13,11 +13,11 @@ import { refreshCartItemsWorkflow } from "./refresh-cart-items" /** * The cart ownership transfer details. */ -export type TransferCartCustomerWorkflowInput = { +export type TransferCartCustomerWorkflowInput = { /** * The cart's ID. */ - id: string; + id: string /** * The ID of the customer to transfer the cart to. */ @@ -29,9 +29,9 @@ export const transferCartCustomerWorkflowId = "transfer-cart-customer" * This workflow transfers a cart's customer ownership to another customer. It's useful if a customer logs in after * adding the items to their cart, allowing you to transfer the cart's ownership to the logged-in customer. This workflow is used * by the [Set Cart's Customer Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidcustomer). - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to set the cart's customer within your custom flows. - * + * * @example * const { result } = await transferCartCustomerWorkflow(container) * .run({ @@ -40,11 +40,11 @@ export const transferCartCustomerWorkflowId = "transfer-cart-customer" * customer_id: "cus_123" * } * }) - * + * * @summary - * + * * Refresh a cart's payment collection details. - * + * * @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution. */ export const transferCartCustomerWorkflow = createWorkflow( @@ -108,7 +108,7 @@ export const transferCartCustomerWorkflow = createWorkflow( updateCartsStep(cartInput) refreshCartItemsWorkflow.runAsStep({ - input: { cart_id: input.id }, + input: { cart_id: input.id, force_refresh: true }, }) } ) 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 dfe2a64e27..487603d028 100644 --- a/packages/core/core-flows/src/cart/workflows/update-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-cart.ts @@ -27,8 +27,8 @@ import { findSalesChannelStep, updateCartsStep, } from "../steps" -import { refreshCartItemsWorkflow } from "./refresh-cart-items" import { validateSalesChannelStep } from "../steps/validate-sales-channel" +import { refreshCartItemsWorkflow } from "./refresh-cart-items" /** * The data to update the cart, along with custom data that's passed to the workflow's hooks. @@ -278,7 +278,11 @@ export const updateCartWorkflow = createWorkflow( }) const cart = refreshCartItemsWorkflow.runAsStep({ - input: { cart_id: cartInput.id, promo_codes: input.promo_codes }, + input: { + cart_id: cartInput.id, + promo_codes: input.promo_codes, + force_refresh: !!newRegion, + }, }) const cartUpdated = createHook("cartUpdated", {