chore(cart, core-flows): Improve tax lines algo management (#11715)

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2025-03-09 12:43:18 +00:00
committed by GitHub
co-authored by Adrien de Peretti
parent 4b3869ef2c
commit b7678983a9
12 changed files with 1051 additions and 144 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/cart": patch
"@medusajs/types": patch
---
chore: improve tax lines
@@ -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<ICartModuleService>(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,
@@ -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<ICartModuleService>(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<ICartModuleService>(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,
}))
}
@@ -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<string, unknown>
@@ -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, {
@@ -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({
@@ -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(
@@ -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<UpsertTaxLinesWorkflowInput>): WorkflowData<void> => {
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,
})
}
)
@@ -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: {
+14 -2
View File
@@ -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<CreateTaxLineDTO, "item_id"> {
/**
* 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<UpdateTaxLineDTO, "item_id"> {
/**
* The associated shipping method's ID.
*/
shipping_method_id?: string
}
/**
* The line item tax line to be created.
+119
View File
@@ -2075,4 +2075,123 @@ export interface ICartModuleService extends IModuleService {
config?: RestoreReturn<TReturnableLinkableKeys>,
sharedContext?: Context
): Promise<Record<TReturnableLinkableKeys, string[]> | 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<LineItemAdjustmentDTO[]>} 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<LineItemAdjustmentDTO[]>
/**
* 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<ShippingMethodAdjustmentDTO[]>} 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<ShippingMethodAdjustmentDTO[]>
/**
* 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<LineItemTaxLineDTO[]>} The line item tax lines.
*
* @example
* const lineItemTaxLines = await orderModuleService
* .upsertLineItemTaxLines(
* [
* {
* code: "123",
* rate: 2
* }
* ]
* )
*
*/
upsertLineItemTaxLines(
taxLines: (CreateLineItemTaxLineDTO | UpdateLineItemTaxLineDTO)[],
sharedContext?: Context
): Promise<LineItemTaxLineDTO[]>
/**
* 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<ShippingMethodTaxLineDTO[]>} 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<ShippingMethodTaxLineDTO[]>
}
@@ -2272,6 +2272,343 @@ moduleIntegrationTestRunner<ICartModuleService>({
})
})
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([
+152 -110
View File
@@ -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<CartTypes.CartLineItemDTO[]>(
items,
{
populate: true,
}
items
)
}
@@ -557,10 +554,7 @@ export default class CartModuleService
)
return await this.baseRepository_.serialize<CartTypes.CartLineItemDTO[]>(
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<CartTypes.LineItemTaxLineDTO[]> {
const result = await this.lineItemTaxLineService_.upsert(
taxLines as CartTypes.UpdateLineItemTaxLineDTO[],
sharedContext
)
return await this.baseRepository_.serialize<CartTypes.LineItemTaxLineDTO[]>(
result
)
}
@InjectTransactionManager()
async upsertLineItemAdjustments(
adjustments: (
| CartTypes.CreateLineItemAdjustmentDTO
| CartTypes.UpdateLineItemAdjustmentDTO
)[],
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.LineItemAdjustmentDTO[]> {
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<CartTypes.ShippingMethodTaxLineDTO[]> {
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<CartTypes.ShippingMethodAdjustmentDTO[]> {
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<CartTypes.ShippingMethodAdjustmentDTO>(
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<CartTypes.LineItemTaxLineDTO[]> {
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<string>(
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<CartTypes.LineItemTaxLineDTO[]>(
result,
{
populate: true,
}
result
)
}
@@ -1159,10 +1208,7 @@ export default class CartModuleService
const serialized =
await this.baseRepository_.serialize<CartTypes.ShippingMethodTaxLineDTO>(
addedTaxLines[0],
{
populate: true,
}
addedTaxLines[0]
)
if (isObject(cartIdOrData)) {
@@ -1181,53 +1227,49 @@ export default class CartModuleService
)[],
@MedusaContext() sharedContext: Context = {}
): Promise<CartTypes.ShippingMethodTaxLineDTO[]> {
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)
}
}