diff --git a/.changeset/afraid-hotels-jam.md b/.changeset/afraid-hotels-jam.md new file mode 100644 index 0000000000..405765cbbe --- /dev/null +++ b/.changeset/afraid-hotels-jam.md @@ -0,0 +1,7 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/cart": patch +"@medusajs/types": patch +--- + +chore: improve tax lines diff --git a/packages/core/core-flows/src/cart/steps/set-tax-lines-for-items.ts b/packages/core/core-flows/src/cart/steps/set-tax-lines-for-items.ts index 635490726a..10df4f4d21 100644 --- a/packages/core/core-flows/src/cart/steps/set-tax-lines-for-items.ts +++ b/packages/core/core-flows/src/cart/steps/set-tax-lines-for-items.ts @@ -6,7 +6,7 @@ import { ItemTaxLineDTO, ShippingTaxLineDTO, } from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" +import { Modules, promiseAll } from "@medusajs/framework/utils" import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" /** @@ -30,13 +30,13 @@ export interface SetTaxLinesForItemsStepInput { export const setTaxLinesForItemsStepId = "set-tax-lines-for-items" /** * This step sets the tax lines of shipping methods and line items in a cart. - * + * * :::tip - * + * * You can use the {@link retrieveCartStep} to retrieve a cart's details. - * + * * ::: - * + * * @example * const data = setTaxLinesForItemsStep({ * // retrieve the details of the cart from another workflow @@ -64,21 +64,31 @@ export const setTaxLinesForItemsStep = createStep( const { cart, item_tax_lines, shipping_tax_lines } = data const cartService = container.resolve(Modules.CART) - const existingShippingMethodTaxLines = - await cartService.listShippingMethodTaxLines({ - shipping_method_id: shipping_tax_lines.map((t) => t.shipping_line_id), - }) + const [existingShippingMethodTaxLines, existingLineItemTaxLines] = + await promiseAll([ + shipping_tax_lines.length + ? cartService.listShippingMethodTaxLines({ + shipping_method_id: shipping_tax_lines.map( + (t) => t.shipping_line_id + ), + }) + : [], - const existingLineItemTaxLines = await cartService.listLineItemTaxLines({ - item_id: item_tax_lines.map((t) => t.line_item_id), - }) + item_tax_lines.length + ? cartService.listLineItemTaxLines({ + item_id: item_tax_lines.map((t) => t.line_item_id), + }) + : [], + ]) const itemsTaxLinesData = normalizeItemTaxLinesForCart(item_tax_lines) - await cartService.setLineItemTaxLines(cart.id, itemsTaxLinesData) - const shippingTaxLinesData = normalizeShippingTaxLinesForCart(shipping_tax_lines) - await cartService.setShippingMethodTaxLines(cart.id, shippingTaxLinesData) + + await promiseAll([ + cartService.setLineItemTaxLines(cart.id, itemsTaxLinesData), + cartService.setShippingMethodTaxLines(cart.id, shippingTaxLinesData), + ]) return new StepResponse(null, { cart, diff --git a/packages/core/core-flows/src/cart/steps/upsert-tax-lines-for-items.ts b/packages/core/core-flows/src/cart/steps/upsert-tax-lines-for-items.ts new file mode 100644 index 0000000000..d04eabfdf2 --- /dev/null +++ b/packages/core/core-flows/src/cart/steps/upsert-tax-lines-for-items.ts @@ -0,0 +1,176 @@ +import { + CartWorkflowDTO, + CreateLineItemTaxLineDTO, + CreateShippingMethodTaxLineDTO, + ICartModuleService, + ItemTaxLineDTO, + LineItemTaxLineDTO, + ShippingMethodTaxLineDTO, + ShippingTaxLineDTO, +} from "@medusajs/framework/types" +import { Modules, promiseAll } from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +/** + * The details of the tax lines to set in a cart. + */ +export interface SetTaxLinesForItemsStepInput { + /** + * The cart's details. + */ + cart: CartWorkflowDTO + /** + * The tax lines to set for line items. + */ + item_tax_lines: ItemTaxLineDTO[] + /** + * The tax lines to set for shipping methods. + */ + shipping_tax_lines: ShippingTaxLineDTO[] +} + +export const upsertTaxLinesForItemsStepId = "set-tax-lines-for-items" +/** + * This step sets the tax lines of shipping methods and line items in a cart. + * + * :::tip + * + * You can use the {@link retrieveCartStep} to retrieve a cart's details. + * + * ::: + * + * @example + * const data = upsertTaxLinesForItemsStep({ + * // retrieve the details of the cart from another workflow + * // or in another step using the Cart Module's service + * cart, + * "item_tax_lines": [{ + * "rate": 48, + * "code": "CODE123", + * "name": "Tax rate 2", + * "provider_id": "provider_1", + * "line_item_id": "litem_123" + * }], + * "shipping_tax_lines": [{ + * "rate": 49, + * "code": "CODE456", + * "name": "Tax rate 1", + * "provider_id": "provider_1", + * "shipping_line_id": "sm_123" + * }] + * }) + */ +export const upsertTaxLinesForItemsStep = createStep( + upsertTaxLinesForItemsStepId, + async (data: SetTaxLinesForItemsStepInput, { container }) => { + const { cart, item_tax_lines, shipping_tax_lines } = data + const cartService = container.resolve(Modules.CART) + + const [existingShippingMethodTaxLines, existingLineItemTaxLines] = + await promiseAll([ + shipping_tax_lines.length + ? cartService.listShippingMethodTaxLines({ + shipping_method_id: shipping_tax_lines.map( + (t) => t.shipping_line_id + ), + }) + : [], + + item_tax_lines.length + ? cartService.listLineItemTaxLines({ + item_id: item_tax_lines.map((t) => t.line_item_id), + }) + : [], + ]) + + const itemsTaxLinesData = normalizeItemTaxLinesForCart( + item_tax_lines, + existingLineItemTaxLines + ) + const shippingTaxLinesData = normalizeShippingTaxLinesForCart( + shipping_tax_lines, + existingShippingMethodTaxLines + ) + + await promiseAll([ + itemsTaxLinesData.length + ? cartService.upsertLineItemTaxLines(itemsTaxLinesData) + : [], + shippingTaxLinesData.length + ? cartService.upsertShippingMethodTaxLines(shippingTaxLinesData) + : [], + ]) + + return new StepResponse(null, { + cart, + existingLineItemTaxLines, + existingShippingMethodTaxLines, + }) + }, + async (revertData, { container }) => { + if (!revertData) { + return + } + + const { existingLineItemTaxLines, existingShippingMethodTaxLines } = + revertData + + const cartService = container.resolve(Modules.CART) + + if (existingLineItemTaxLines) { + await cartService.upsertLineItemTaxLines( + existingLineItemTaxLines.map((taxLine) => ({ + description: taxLine.description, + tax_rate_id: taxLine.tax_rate_id, + code: taxLine.code, + rate: taxLine.rate, + provider_id: taxLine.provider_id, + item_id: taxLine.item_id, + })) + ) + } + + await cartService.upsertShippingMethodTaxLines( + existingShippingMethodTaxLines.map((taxLine) => ({ + description: taxLine.description, + tax_rate_id: taxLine.tax_rate_id, + code: taxLine.code, + rate: taxLine.rate, + provider_id: taxLine.provider_id, + shipping_method_id: taxLine.shipping_method_id, + })) + ) + } +) + +function normalizeItemTaxLinesForCart( + taxLines: ItemTaxLineDTO[], + existingTaxLines: LineItemTaxLineDTO[] +): CreateLineItemTaxLineDTO[] { + return taxLines.map((taxLine: ItemTaxLineDTO & { id?: string }) => ({ + id: existingTaxLines.find((t) => t.item_id === taxLine.line_item_id)?.id, + description: taxLine.name, + tax_rate_id: taxLine.rate_id, + code: taxLine.code!, + rate: taxLine.rate!, + provider_id: taxLine.provider_id, + item_id: taxLine.line_item_id, + })) +} + +function normalizeShippingTaxLinesForCart( + taxLines: ShippingTaxLineDTO[], + existingTaxLines: ShippingMethodTaxLineDTO[] +): CreateShippingMethodTaxLineDTO[] { + return taxLines.map((taxLine: ShippingTaxLineDTO & { id?: string }) => ({ + id: existingTaxLines.find( + (t) => t.shipping_method_id === taxLine.shipping_line_id + )?.id, + description: taxLine.name, + tax_rate_id: taxLine.rate_id, + code: taxLine.code!, + rate: taxLine.rate!, + provider_id: taxLine.provider_id, + shipping_method_id: taxLine.shipping_line_id, + })) +} 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 dfb991dedb..f0efa838d1 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 @@ -39,7 +39,7 @@ export interface AddShippingMethodToCartWorkflowInput { id: string /** * Custom data useful for the fulfillment provider processing the shipping option or method. - * + * * Learn more in [this documentation](https://docs.medusajs.com/resources/commerce-modules/fulfillment/shipping-option#data-property). */ data?: Record @@ -48,11 +48,11 @@ export interface AddShippingMethodToCartWorkflowInput { export const addShippingMethodToCartWorkflowId = "add-shipping-method-to-cart" /** - * This workflow adds a shipping method to a cart. It's executed by the + * This workflow adds a shipping method to a cart. It's executed by the * [Add Shipping Method Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods). - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around adding a shipping method to the cart. - * + * * @example * const { result } = await addShippingMethodToCartWorkflow(container) * .run({ @@ -71,11 +71,11 @@ export const addShippingMethodToCartWorkflowId = "add-shipping-method-to-cart" * ] * } * }) - * + * * @summary - * + * * Add a shipping method to a cart. - * + * * @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 addShippingMethodToCartWorkflow = createWorkflow( @@ -186,7 +186,7 @@ export const addShippingMethodToCartWorkflow = createWorkflow( cart.shipping_methods.map((sm) => sm.id) ) - parallelize( + const [, createdShippingMethods] = parallelize( removeShippingMethodFromCartStep({ shipping_method_ids: currentShippingMethods, }), @@ -200,7 +200,7 @@ export const addShippingMethodToCartWorkflow = createWorkflow( ) refreshCartItemsWorkflow.runAsStep({ - input: { cart_id: cart.id }, + input: { cart_id: cart.id, shipping_methods: createdShippingMethods }, }) return new WorkflowResponse(void 0, { 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 3d566be1bd..554856dfe3 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 @@ -173,7 +173,7 @@ export const addToCartWorkflow = createWorkflow( }, }) - parallelize( + const [createdLineItems, updatedLineItems] = parallelize( createLineItemsStep({ id: cart.id, items: itemsToCreate, @@ -184,8 +184,15 @@ export const addToCartWorkflow = createWorkflow( }) ) + const allItems = transform( + { createdLineItems, updatedLineItems }, + ({ createdLineItems = [], updatedLineItems = [] }) => { + return createdLineItems.concat(updatedLineItems) + } + ) + refreshCartItemsWorkflow.runAsStep({ - input: { cart_id: cart.id }, + input: { cart_id: cart.id, items: allItems }, }) emitEventStep({ 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 701e93cfe8..b64b090cc6 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 @@ -26,6 +26,7 @@ import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-meth import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection" import { updateCartPromotionsWorkflow } from "./update-cart-promotions" import { updateTaxLinesWorkflow } from "./update-tax-lines" +import { upsertTaxLinesWorkflow } from "./upsert-tax-lines" /** * The details of the cart to refresh. @@ -44,6 +45,23 @@ export type RefreshCartItemsWorkflowInput = { * Force refresh the cart items */ force_refresh?: boolean + + /** + * The items to refresh. + */ + items?: any[] + + /** + * The shipping methods to refresh. + */ + shipping_methods?: any[] + + /** + * Whether to force re-calculating tax amounts, which + * may include sending requests to a third-part tax provider, depending + * on the configurations of the cart's tax region. + */ + force_tax_calculation?: boolean } export const refreshCartItemsWorkflowId = "refresh-cart-items" @@ -162,8 +180,33 @@ export const refreshCartItemsWorkflow = createWorkflow( input: refreshCartInput, }) - updateTaxLinesWorkflow.runAsStep({ - input: refreshCartInput, + when({ input }, ({ input }) => { + return !!input.force_refresh + }).then(() => { + updateTaxLinesWorkflow.runAsStep({ + input: refreshCartInput, + }) + }) + + when({ input }, ({ input }) => { + return ( + !input.force_refresh && + (!!input.items?.length || !!input.shipping_methods?.length) + ) + }).then(() => { + upsertTaxLinesWorkflow.runAsStep({ + input: transform( + { refetchedCart, input }, + ({ refetchedCart, input }) => { + return { + cart: refetchedCart, + items: input.items ?? [], + shipping_methods: input.shipping_methods ?? [], + force_tax_calculation: input.force_tax_calculation, + } + } + ), + }) }) const cartPromoCodes = transform( diff --git a/packages/core/core-flows/src/cart/workflows/upsert-tax-lines.ts b/packages/core/core-flows/src/cart/workflows/upsert-tax-lines.ts new file mode 100644 index 0000000000..d297b2a45c --- /dev/null +++ b/packages/core/core-flows/src/cart/workflows/upsert-tax-lines.ts @@ -0,0 +1,154 @@ +import { + CartLineItemDTO, + CartShippingMethodDTO, +} from "@medusajs/framework/types" +import { + WorkflowData, + createWorkflow, + transform, + when, +} from "@medusajs/framework/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { getItemTaxLinesStep } from "../../tax/steps/get-item-tax-lines" +import { upsertTaxLinesForItemsStep } from "../steps/upsert-tax-lines-for-items" + +const cartFields = [ + "id", + "currency_code", + "email", + "region.id", + "region.automatic_taxes", + "items.id", + "items.variant_id", + "items.product_id", + "items.product_title", + "items.product_description", + "items.product_subtitle", + "items.product_type", + "items.product_type_id", + "items.product_collection", + "items.product_handle", + "items.variant_sku", + "items.variant_barcode", + "items.variant_title", + "items.title", + "items.quantity", + "items.unit_price", + "items.tax_lines.id", + "items.tax_lines.description", + "items.tax_lines.code", + "items.tax_lines.rate", + "items.tax_lines.provider_id", + "shipping_methods.tax_lines.id", + "shipping_methods.tax_lines.description", + "shipping_methods.tax_lines.code", + "shipping_methods.tax_lines.rate", + "shipping_methods.tax_lines.provider_id", + "shipping_methods.shipping_option_id", + "shipping_methods.amount", + "customer.id", + "customer.email", + "customer.metadata", + "customer.groups.id", + "shipping_address.id", + "shipping_address.address_1", + "shipping_address.address_2", + "shipping_address.city", + "shipping_address.postal_code", + "shipping_address.country_code", + "shipping_address.region_code", + "shipping_address.province", + "shipping_address.metadata", +] + +/** + * The details of the cart to upsert tax lines for. + */ +export type UpsertTaxLinesWorkflowInput = { + /** + * The cart's ID. + */ + cart_id?: string + /** + * The Cart reference. + */ + cart?: any + /** + * The items to upsert their tax lines. + * If not specified, taxes are upsertd for all of the cart's + * line items. + */ + items: CartLineItemDTO[] + /** + * The shipping methods to upsert their tax lines. + * If not specified, taxes are upsertd for all of the cart's + * shipping methods. + */ + shipping_methods: CartShippingMethodDTO[] + + /** + * Whether to force re-calculating tax amounts, which + * may include sending requests to a third-part tax provider, depending + * on the configurations of the cart's tax region. + * + * @defaultValue false + */ + force_tax_calculation?: boolean +} + +export const upsertTaxLinesWorkflowId = "upsert-tax-lines" +/** + * This workflow upserts a cart's tax lines that are applied on line items and shipping methods. You can upsert the line item's quantity, unit price, and more. This workflow is executed + * by the [Calculate Taxes Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidtaxes). + * + * You can use this workflow within your own customizations or custom workflows, allowing you to upsert a cart's tax lines in your custom flows. + * + * @example + * const { result } = await upsertTaxLinesWorkflow(container) + * .run({ + * input: { + * cart_id: "cart_123", + * items: [], + * shipping_methods: [], + * } + * }) + * + * @summary + * + * Update a cart's tax lines. + */ +export const upsertTaxLinesWorkflow = createWorkflow( + upsertTaxLinesWorkflowId, + (input: WorkflowData): WorkflowData => { + const fetchCart = when({ input }, ({ input }) => { + return !input.cart + }).then(() => { + return useRemoteQueryStep({ + entry_point: "cart", + fields: cartFields, + 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 taxLineItems = getItemTaxLinesStep( + transform({ input, cart }, (data) => ({ + orderOrCart: data.cart, + items: data.input.items ?? [], + shipping_methods: data.input.shipping_methods ?? [], + force_tax_calculation: data.input.force_tax_calculation, + })) + ) + + upsertTaxLinesForItemsStep({ + cart, + item_tax_lines: taxLineItems.lineItemTaxLines, + shipping_tax_lines: taxLineItems.shippingMethodsTaxLines, + }) + } +) diff --git a/packages/core/core-flows/src/tax/steps/get-item-tax-lines.ts b/packages/core/core-flows/src/tax/steps/get-item-tax-lines.ts index 71ba06ccdb..b1e377f0e5 100644 --- a/packages/core/core-flows/src/tax/steps/get-item-tax-lines.ts +++ b/packages/core/core-flows/src/tax/steps/get-item-tax-lines.ts @@ -129,14 +129,14 @@ function normalizeLineItemsForShipping( export const getItemTaxLinesStepId = "get-item-tax-lines" /** * This step retrieves the tax lines for an order or cart's line items and shipping methods. - * + * * :::note - * + * * You can retrieve an order, cart, item, shipping method, and address details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = getItemTaxLinesStep({ * orderOrCart: { diff --git a/packages/core/types/src/cart/mutations.ts b/packages/core/types/src/cart/mutations.ts index b5b52a53e4..093e1047df 100644 --- a/packages/core/types/src/cart/mutations.ts +++ b/packages/core/types/src/cart/mutations.ts @@ -430,12 +430,24 @@ export interface UpdateTaxLineDTO { /** * The shipping method tax line to be created. */ -export interface CreateShippingMethodTaxLineDTO extends CreateTaxLineDTO {} +export interface CreateShippingMethodTaxLineDTO + extends Omit { + /** + * The associated shipping method's ID. + */ + shipping_method_id: string +} /** * The attributes to update in the shipping method tax line. */ -export interface UpdateShippingMethodTaxLineDTO extends UpdateTaxLineDTO {} +export interface UpdateShippingMethodTaxLineDTO + extends Omit { + /** + * The associated shipping method's ID. + */ + shipping_method_id?: string +} /** * The line item tax line to be created. diff --git a/packages/core/types/src/cart/service.ts b/packages/core/types/src/cart/service.ts index 9d4ac7dbd3..54232518ad 100644 --- a/packages/core/types/src/cart/service.ts +++ b/packages/core/types/src/cart/service.ts @@ -2075,4 +2075,123 @@ export interface ICartModuleService extends IModuleService { config?: RestoreReturn, sharedContext?: Context ): Promise | void> + + /** + * This method upserts line item adjustments. + * + * @param {UpsertLineItemAdjustmentDTO[]} data - The line item adjustments to create or update. If the `id` property is provided + * in an object, it means an existing line item adjustment will be updated. Otherwise, a new one is created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The line item adjustments. + * + * @example + * const lineItemAdjustments = await orderModuleService.upsertLineItemAdjustments( + * [ + * { + * item_id: "1234", + * amount: 10 + * }, + * { + * id: "123", + * item_id: "4321", + * amount: 20 + * } + * ] + * ) + * + */ + upsertLineItemAdjustments( + data: UpsertLineItemAdjustmentDTO[], + sharedContext?: Context + ): Promise + + /** + * This method upserts shipping method adjustments. + * + * @param {(CreateShippingMethodAdjustmentDTO | UpdateShippingMethodAdjustmentDTO)[]} data - The shipping method adjustments to be created + * or updated. If an adjustment object has an `id` property, it's updated. Otherwise, a new adjustment is created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The shipping method adjustments. + * + * @example + * const shippingMethodAdjustments = await orderModuleService + * .upsertShippingMethodAdjustments( + * [ + * { + * shipping_method_id: "123", + * code: "50OFF", + * amount: 5 + * }, + * { + * id: "321", + * amount: 5 + * } + * ] + * ) + * + */ + upsertShippingMethodAdjustments( + data: ( + | CreateShippingMethodAdjustmentDTO + | UpdateShippingMethodAdjustmentDTO + )[], + sharedContext?: Context + ): Promise + + /** + * This method upserts line item tax lines. + * + * @param {(CreateLineItemTaxLineDTO | UpdateLineItemTaxLineDTO)[]} taxLines - The line item tax lines to create or update. If the + * tax line object has an `id` property, it'll be updated. Otherwise, a tax line is created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The line item tax lines. + * + * @example + * const lineItemTaxLines = await orderModuleService + * .upsertLineItemTaxLines( + * [ + * { + * code: "123", + * rate: 2 + * } + * ] + * ) + * + */ + upsertLineItemTaxLines( + taxLines: (CreateLineItemTaxLineDTO | UpdateLineItemTaxLineDTO)[], + sharedContext?: Context + ): Promise + + /** + * This method upsert shipping method tax lines. + * + * @param {(CreateShippingMethodTaxLineDTO | UpdateShippingMethodTaxLineDTO)[]} taxLines - The shipping method tax lines to create or update. + * If a tax line object has an `id` property, it's updated. Otherwise, a tax line is created. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The shipping method tax lines. + * + * @example + * const shippingMethodTaxLines = await orderModuleService + * .upsertShippingMethodTaxLines( + * [ + * { + * code: "123", + * rate: 2 + * }, + * { + * id: "321", + * rate: 2 + * } + * ] + * ) + * + */ + upsertShippingMethodTaxLines( + taxLines: ( + | CreateShippingMethodTaxLineDTO + | UpdateShippingMethodTaxLineDTO + )[], + sharedContext?: Context + ): Promise } diff --git a/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index 0363633456..bdfa87d96e 100644 --- a/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -2272,6 +2272,343 @@ moduleIntegrationTestRunner({ }) }) + describe("setShippingMethodTaxLines", () => { + it("should set shipping item tax lines for a cart", async () => { + const [createdCart] = await service.createCarts([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addShippingMethods(createdCart.id, [ + { + name: "test", + amount: 100, + }, + ]) + + const [itemTwo] = await service.addShippingMethods(createdCart.id, [ + { + name: "test-2", + amount: 200, + }, + ]) + + const taxLines = await service.setShippingMethodTaxLines( + createdCart.id, + [ + { + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }, + { + shipping_method_id: itemTwo.id, + rate: 20, + code: "TX", + }, + ] + ) + + expect(taxLines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }), + expect.objectContaining({ + shipping_method_id: itemTwo.id, + rate: 20, + code: "TX", + }), + ]) + ) + }) + + it("should replace shipping item tax lines for a cart", async () => { + const [createdCart] = await service.createCarts([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addShippingMethods(createdCart.id, [ + { + name: "test", + amount: 100, + }, + ]) + + const taxLines = await service.setShippingMethodTaxLines( + createdCart.id, + [ + { + shipping_method_id: itemOne.id, + rate: 20.753, + code: "TX", + }, + ] + ) + + expect(taxLines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 20.753, + code: "TX", + }), + ]) + ) + + await service.setShippingMethodTaxLines(createdCart.id, [ + { + shipping_method_id: itemOne.id, + rate: 25.14789, + code: "TX-2", + }, + ]) + + const cart = await service.retrieveCart(createdCart.id, { + relations: ["shipping_methods.tax_lines"], + }) + + expect(cart.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 25.14789, + code: "TX-2", + }), + ]), + }), + ]) + ) + + expect(cart.shipping_methods?.length).toBe(1) + expect(cart.shipping_methods?.[0].tax_lines?.length).toBe(1) + }) + + it("should remove all shipping item tax lines for a cart", async () => { + const [createdCart] = await service.createCarts([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addShippingMethods(createdCart.id, [ + { + name: "test", + amount: 100, + }, + ]) + + const taxLines = await service.setShippingMethodTaxLines( + createdCart.id, + [ + { + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }, + ] + ) + + expect(taxLines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }), + ]) + ) + + await service.setShippingMethodTaxLines(createdCart.id, []) + + const cart = await service.retrieveCart(createdCart.id, { + relations: ["shipping_methods.tax_lines"], + }) + + expect(cart.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + tax_lines: [], + }), + ]) + ) + + expect(cart.shipping_methods?.length).toBe(1) + expect(cart.shipping_methods?.[0].tax_lines?.length).toBe(0) + }) + + it("should update shipping item tax lines for a cart", async () => { + const [createdCart] = await service.createCarts([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addShippingMethods(createdCart.id, [ + { + name: "test", + amount: 100, + }, + ]) + + const taxLines = await service.setShippingMethodTaxLines( + createdCart.id, + [ + { + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }, + ] + ) + + expect(taxLines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }), + ]) + ) + + await service.setShippingMethodTaxLines(createdCart.id, [ + { + id: taxLines[0].id, + shipping_method_id: itemOne.id, + rate: 25, + code: "TX", + }, + ]) + + const cart = await service.retrieveCart(createdCart.id, { + relations: ["shipping_methods.tax_lines"], + }) + + expect(cart.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + tax_lines: [ + expect.objectContaining({ + id: taxLines[0].id, + shipping_method_id: itemOne.id, + rate: 25, + code: "TX", + }), + ], + }), + ]) + ) + + expect(cart.shipping_methods?.length).toBe(1) + expect(cart.shipping_methods?.[0].tax_lines?.length).toBe(1) + }) + + it("should remove, update, and create shipping item tax lines for a cart", async () => { + const [createdCart] = await service.createCarts([ + { + currency_code: "eur", + }, + ]) + + const [itemOne] = await service.addShippingMethods(createdCart.id, [ + { + name: "test", + amount: 100, + }, + ]) + + const taxLines = await service.setShippingMethodTaxLines( + createdCart.id, + [ + { + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }, + { + shipping_method_id: itemOne.id, + rate: 25, + code: "TX", + }, + ] + ) + + expect(taxLines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 20, + code: "TX", + }), + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 25, + code: "TX", + }), + ]) + ) + + const taxLine = taxLines.find( + (tx) => tx.shipping_method_id === itemOne.id + ) + + await service.setShippingMethodTaxLines(createdCart.id, [ + // update + { + id: taxLine.id, + rate: 40, + code: "TX", + }, + // create + { + shipping_method_id: itemOne.id, + rate: 25, + code: "TX-2", + }, + // remove: should remove the initial tax line for itemOne + ]) + + const cart = await service.retrieveCart(createdCart.id, { + relations: ["shipping_methods.tax_lines"], + }) + + expect(cart.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: itemOne.id, + tax_lines: [ + expect.objectContaining({ + id: taxLine!.id, + shipping_method_id: itemOne.id, + rate: 40, + code: "TX", + }), + expect.objectContaining({ + shipping_method_id: itemOne.id, + rate: 25, + code: "TX-2", + }), + ], + }), + ]) + ) + + expect(cart.shipping_methods?.length).toBe(1) + expect(cart.shipping_methods?.[0].tax_lines?.length).toBe(2) + }) + }) + describe("addLineItemAdjustments", () => { it("should add line item tax lines for items in a cart", async () => { const [createdCart] = await service.createCarts([ diff --git a/packages/modules/cart/src/services/cart-module.ts b/packages/modules/cart/src/services/cart-module.ts index ebac8b3eb8..74f12bec28 100644 --- a/packages/modules/cart/src/services/cart-module.ts +++ b/packages/modules/cart/src/services/cart-module.ts @@ -13,6 +13,7 @@ import { createRawPropertiesFromBigNumber, decorateCartTotals, deduplicate, + generateEntityId, InjectManager, InjectTransactionManager, isObject, @@ -20,6 +21,7 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, + promiseAll, } from "@medusajs/framework/utils" import { Address, @@ -357,9 +359,7 @@ export default class CartModuleService const serializedResult = await this.baseRepository_.serialize< CartTypes.CartDTO[] - >(result, { - populate: true, - }) + >(result) return isString(dataOrIdOrSelector) ? serializedResult[0] : serializedResult } @@ -448,10 +448,7 @@ export default class CartModuleService } return await this.baseRepository_.serialize( - items, - { - populate: true, - } + items ) } @@ -557,10 +554,7 @@ export default class CartModuleService ) return await this.baseRepository_.serialize( - items, - { - populate: true, - } + items ) } @@ -718,7 +712,7 @@ export default class CartModuleService return await this.baseRepository_.serialize< CartTypes.CartShippingMethodDTO[] - >(methods, { populate: true }) + >(methods) } @InjectTransactionManager() @@ -809,9 +803,79 @@ export default class CartModuleService return await this.baseRepository_.serialize< CartTypes.LineItemAdjustmentDTO[] - >(addedAdjustments, { - populate: true, - }) + >(addedAdjustments) + } + + @InjectTransactionManager() + async upsertLineItemTaxLines( + taxLines: ( + | CartTypes.CreateLineItemTaxLineDTO + | CartTypes.UpdateLineItemTaxLineDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.lineItemTaxLineService_.upsert( + taxLines as CartTypes.UpdateLineItemTaxLineDTO[], + sharedContext + ) + + return await this.baseRepository_.serialize( + result + ) + } + + @InjectTransactionManager() + async upsertLineItemAdjustments( + adjustments: ( + | CartTypes.CreateLineItemAdjustmentDTO + | CartTypes.UpdateLineItemAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + let result = await this.lineItemAdjustmentService_.upsert( + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.LineItemAdjustmentDTO[] + >(result) + } + + @InjectTransactionManager() + async upsertShippingMethodTaxLines( + taxLines: ( + | CartTypes.CreateShippingMethodTaxLineDTO + | CartTypes.UpdateShippingMethodTaxLineDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.shippingMethodTaxLineService_.upsert( + taxLines as UpdateShippingMethodTaxLineDTO[], + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.ShippingMethodTaxLineDTO[] + >(result) + } + + @InjectTransactionManager() + async upsertShippingMethodAdjustments( + adjustments: ( + | CartTypes.CreateShippingMethodAdjustmentDTO + | CartTypes.UpdateShippingMethodAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.shippingMethodAdjustmentService_.upsert( + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.ShippingMethodAdjustmentDTO[] + >(result) } @InjectTransactionManager() @@ -864,9 +928,7 @@ export default class CartModuleService return await this.baseRepository_.serialize< CartTypes.LineItemAdjustmentDTO[] - >(result, { - populate: true, - }) + >(result) } @InjectTransactionManager() @@ -921,9 +983,7 @@ export default class CartModuleService return await this.baseRepository_.serialize< CartTypes.ShippingMethodAdjustmentDTO[] - >(result, { - populate: true, - }) + >(result) } async addShippingMethodAdjustments( @@ -986,17 +1046,13 @@ export default class CartModuleService if (isObject(cartIdOrData)) { return await this.baseRepository_.serialize( addedAdjustments[0], - { - populate: true, - } + {} ) } return await this.baseRepository_.serialize< CartTypes.ShippingMethodAdjustmentDTO[] - >(addedAdjustments, { - populate: true, - }) + >(addedAdjustments) } addLineItemTaxLines( @@ -1046,9 +1102,7 @@ export default class CartModuleService const serialized = await this.baseRepository_.serialize< CartTypes.LineItemTaxLineDTO[] - >(addedTaxLines, { - populate: true, - }) + >(addedTaxLines) if (isObject(cartIdOrData)) { return serialized[0] @@ -1066,49 +1120,44 @@ export default class CartModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { - const cart = await this.retrieveCart( - cartId, - { select: ["id"], relations: ["items.tax_lines"] }, - sharedContext - ) - - const existingTaxLines = await this.listLineItemTaxLines( - { item: { cart_id: cart.id } }, - { select: ["id"] }, - sharedContext - ) - - const taxLinesSet = new Set( - taxLines - .map((taxLine) => (taxLine as CartTypes.UpdateLineItemTaxLineDTO)?.id) - .filter(Boolean) - ) - - const toDelete: CartTypes.LineItemTaxLineDTO[] = [] - - // From the existing tax lines, find the ones that are not passed in taxLines - existingTaxLines.forEach((taxLine: CartTypes.LineItemTaxLineDTO) => { - if (!taxLinesSet.has(taxLine.id)) { - toDelete.push(taxLine) - } + const normalizedTaxLines = ( + taxLines as CartTypes.UpdateLineItemTaxLineDTO[] + ).map((taxLine) => { + // Pre generate the id so that we can optimized the actions below + taxLine.id = generateEntityId(taxLine.id, "calitxl") + return taxLine }) - if (toDelete.length) { - await this.lineItemTaxLineService_.softDelete( - toDelete.map((taxLine) => taxLine!.id), - sharedContext + const taxLineIdsSet = new Set( + normalizedTaxLines.map( + (taxLine) => (taxLine as CartTypes.UpdateLineItemTaxLineDTO)?.id ) + ) + + const deleteConstraints: { + id?: { + $nin: string[] + } + item: { cart_id: string } + } = { + item: { cart_id: cartId }, } - const result = taxLines.length - ? await this.lineItemTaxLineService_.upsert(taxLines, sharedContext) - : [] + if (taxLineIdsSet.size) { + deleteConstraints.id = { + $nin: Array.from(taxLineIdsSet), + } + } + + const [result] = await promiseAll([ + normalizedTaxLines.length + ? this.lineItemTaxLineService_.upsert(normalizedTaxLines, sharedContext) + : [], + this.lineItemTaxLineService_.softDelete(deleteConstraints, sharedContext), + ]) return await this.baseRepository_.serialize( - result, - { - populate: true, - } + result ) } @@ -1159,10 +1208,7 @@ export default class CartModuleService const serialized = await this.baseRepository_.serialize( - addedTaxLines[0], - { - populate: true, - } + addedTaxLines[0] ) if (isObject(cartIdOrData)) { @@ -1181,53 +1227,49 @@ export default class CartModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { - const cart = await this.retrieveCart( - cartId, - { select: ["id"], relations: ["shipping_methods.tax_lines"] }, - sharedContext - ) - - const existingTaxLines = await this.listShippingMethodTaxLines( - { shipping_method: { cart_id: cart.id } }, - { select: ["id"] }, - sharedContext - ) - - const taxLinesSet = new Set( - taxLines - .map( - (taxLine) => (taxLine as CartTypes.UpdateShippingMethodTaxLineDTO)?.id - ) - .filter(Boolean) - ) - - const toDelete: CartTypes.ShippingMethodTaxLineDTO[] = [] - - // From the existing tax lines, find the ones that are not passed in taxLines - existingTaxLines.forEach((taxLine: CartTypes.ShippingMethodTaxLineDTO) => { - if (!taxLinesSet.has(taxLine.id)) { - toDelete.push(taxLine) - } + const normalizedTaxLines = ( + taxLines as CartTypes.UpdateShippingMethodTaxLineDTO[] + ).map((taxLine) => { + taxLine.id = generateEntityId(taxLine.id, "casmtxl") + return taxLine }) - if (toDelete.length) { - await this.shippingMethodTaxLineService_.softDelete( - toDelete.map((taxLine) => taxLine!.id), - sharedContext + const taxLineIdsSet = new Set( + normalizedTaxLines.map( + (taxLine) => (taxLine as CartTypes.UpdateShippingMethodTaxLineDTO)?.id ) + ) + + const deleteConstraints: { + id?: { + $nin: string[] + } + shipping_method: { cart_id: string } + } = { + shipping_method: { cart_id: cartId }, } - const result = taxLines.length - ? await this.shippingMethodTaxLineService_.upsert( - taxLines as UpdateShippingMethodTaxLineDTO[], - sharedContext - ) - : [] + if (taxLineIdsSet.size) { + deleteConstraints.id = { + $nin: Array.from(taxLineIdsSet), + } + } + + const [result] = await promiseAll([ + taxLines.length + ? this.shippingMethodTaxLineService_.upsert( + taxLines as UpdateShippingMethodTaxLineDTO[], + sharedContext + ) + : [], + this.shippingMethodTaxLineService_.softDelete( + deleteConstraints, + sharedContext + ), + ]) return await this.baseRepository_.serialize< CartTypes.ShippingMethodTaxLineDTO[] - >(result, { - populate: true, - }) + >(result) } }