feat(core-flows,types,utils,medusa): Translate tax lines (#14359)

* Include locale field for traslations on tax line workflows

* Translate tax lines in getItemTaxLinesStep with new util

* Update tax calculation context, so that we pass locale to third party tax providers if they want to return translated tax rates

* Apply translations to tax lines on product and variant tax middlewares

* Cart management translations tests

* Update tax lines when order locale gets updated

* Add changeset

* Get tranlsated tax lines step

* Fix wording

* Mutate ref directly

* Update order tax lines translations upon order locale change

* Claims translations tests

* Update tax lines upon draft order locale update

* Exchange tests for tax lines translations

* Order edits test for tax line translation

* Add tests for shipping methods tax line translations on various order flows

* Returns shipping method translations tests

* Execute update in parallel

* Use TranslationFeatureFlag.key

* Fix feature flag import

* Add @medusajs/medusa dependency for feature flag usage

* Revert "Add @medusajs/medusa dependency for feature flag usage"

This reverts commit e8897aed0a88f83c1034ac73e817e4222250a2c9.

* Use feature flag string directly

* Fix test

* Parallelize tax line translations application
This commit is contained in:
Nicolas Gorga
2026-01-13 15:12:42 -03:00
committed by GitHub
parent 28fae96cee
commit cec8b8e428
23 changed files with 2032 additions and 164 deletions

View File

@@ -1,6 +1,8 @@
import {
CartLineItemDTO,
CartShippingMethodDTO,
ItemTaxLineDTO,
ShippingTaxLineDTO,
} from "@medusajs/framework/types"
import {
WorkflowData,
@@ -12,11 +14,13 @@ import { useQueryGraphStep } from "../../common"
import { acquireLockStep, releaseLockStep } from "../../locking"
import { getItemTaxLinesStep } from "../../tax/steps/get-item-tax-lines"
import { setTaxLinesForItemsStep, validateCartStep } from "../steps"
import { getTranslatedTaxLinesStep } from "../../common/steps/get-translated-tax-lines"
const cartFields = [
"id",
"currency_code",
"email",
"locale",
"region.id",
"region.automatic_taxes",
"items.id",
@@ -161,10 +165,17 @@ export const updateTaxLinesWorkflow = createWorkflow(
}))
)
const translatedTaxLines = getTranslatedTaxLinesStep({
itemTaxLines: taxLineItems.lineItemTaxLines,
shippingTaxLines: taxLineItems.shippingMethodsTaxLines,
locale: cart.locale,
})
setTaxLinesForItemsStep({
cart,
item_tax_lines: taxLineItems.lineItemTaxLines,
shipping_tax_lines: taxLineItems.shippingMethodsTaxLines,
item_tax_lines: translatedTaxLines.itemTaxLines as ItemTaxLineDTO[],
shipping_tax_lines:
translatedTaxLines.shippingTaxLines as ShippingTaxLineDTO[],
})
releaseLockStep({

View File

@@ -1,6 +1,8 @@
import {
CartLineItemDTO,
CartShippingMethodDTO,
ItemTaxLineDTO,
ShippingTaxLineDTO,
} from "@medusajs/framework/types"
import {
WorkflowData,
@@ -12,9 +14,11 @@ import { useQueryGraphStep } from "../../common"
import { getItemTaxLinesStep } from "../../tax/steps/get-item-tax-lines"
import { validateCartStep } from "../steps"
import { upsertTaxLinesForItemsStep } from "../steps/upsert-tax-lines-for-items"
import { getTranslatedTaxLinesStep } from "../../common/steps/get-translated-tax-lines"
const cartFields = [
"id",
"locale",
"currency_code",
"email",
"region.id",
@@ -153,10 +157,17 @@ export const upsertTaxLinesWorkflow = createWorkflow(
}))
)
const translatedTaxLines = getTranslatedTaxLinesStep({
itemTaxLines: taxLineItems.lineItemTaxLines,
shippingTaxLines: taxLineItems.shippingMethodsTaxLines,
locale: cart.locale,
})
upsertTaxLinesForItemsStep({
cart,
item_tax_lines: taxLineItems.lineItemTaxLines,
shipping_tax_lines: taxLineItems.shippingMethodsTaxLines,
item_tax_lines: translatedTaxLines.itemTaxLines as ItemTaxLineDTO[],
shipping_tax_lines:
translatedTaxLines.shippingTaxLines as ShippingTaxLineDTO[],
})
}
)

View File

@@ -0,0 +1,41 @@
import { ItemTaxLineDTO, ShippingTaxLineDTO } from "@medusajs/framework/types"
import {
applyTranslationsToTaxLines,
FeatureFlag,
} from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export const getTranslatedTaxLinesStepId = "get-translated-tax-lines-step"
export interface GetTranslatedTaxLinesStepInput {
itemTaxLines: ItemTaxLineDTO[]
shippingTaxLines: ShippingTaxLineDTO[]
locale: string
}
export const getTranslatedTaxLinesStep = createStep(
getTranslatedTaxLinesStepId,
async (
{ itemTaxLines, shippingTaxLines, locale }: GetTranslatedTaxLinesStepInput,
{ container }
) => {
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
if (!isTranslationEnabled) {
return new StepResponse({
itemTaxLines,
shippingTaxLines,
})
}
const [translatedItemTaxLines, translatedShippingTaxLines] =
await Promise.all([
applyTranslationsToTaxLines(itemTaxLines, locale, container),
applyTranslationsToTaxLines(shippingTaxLines, locale, container),
])
return new StepResponse({
itemTaxLines: translatedItemTaxLines,
shippingTaxLines: translatedShippingTaxLines,
})
}
)

View File

@@ -25,6 +25,7 @@ import {
updateOrderShippingMethodsTranslationsStep,
} from "../../order"
import { validateDraftOrderStep } from "../steps/validate-draft-order"
import { updateOrderTaxLinesTranslationsStep } from "../../order/steps/update-order-tax-lines-translations"
export const updateDraftOrderWorkflowId = "update-draft-order"
@@ -350,6 +351,10 @@ export const updateDraftOrderWorkflow = createWorkflow(
locale: input.locale!,
shippingMethods: order.shipping_methods,
}),
updateOrderTaxLinesTranslationsStep({
order_id: input.id,
locale: input.locale!,
}),
updateOrderItemsTranslationsStep({
order_id: input.id,
locale: input.locale!,

View File

@@ -0,0 +1,119 @@
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import {
applyTranslations,
ContainerRegistrationKeys,
FeatureFlag,
Modules,
} from "@medusajs/framework/utils"
export const updateOrderTaxLinesTranslationsStepId =
"update-order-tax-lines-translations"
interface UpdateOrderTaxLinesTranslationsStepInput {
order_id: string
locale: string
}
export const updateOrderTaxLinesTranslationsStep = createStep(
updateOrderTaxLinesTranslationsStepId,
async (data: UpdateOrderTaxLinesTranslationsStepInput, { container }) => {
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
if (!isTranslationEnabled || !data.locale) {
return new StepResponse(void 0, [])
}
const {
data: [order],
} = await query.graph({
entity: "order",
filters: { id: data.order_id },
fields: [
"items.tax_lines.id",
"items.tax_lines.tax_rate_id",
"items.tax_lines.description",
"shipping_methods.tax_lines.id",
"shipping_methods.tax_lines.tax_rate_id",
"shipping_methods.tax_lines.description",
],
})
const orderModuleService = container.resolve(Modules.ORDER)
const originalItemTaxLines = order.items.flatMap((item) => item.tax_lines)
const originalShippingMethodsTaxLines = order.shipping_methods.flatMap(
(shippingMethod) => shippingMethod.tax_lines
)
const translatedItemsTaxRates = originalItemTaxLines.map((taxLine) => ({
id: taxLine.tax_rate_id,
name: taxLine.description,
tax_line_id: taxLine.id,
}))
await applyTranslations({
localeCode: data.locale,
objects: translatedItemsTaxRates,
container,
})
const translatedShippingMethodsTaxRates =
originalShippingMethodsTaxLines.map((taxLine) => ({
id: taxLine.tax_rate_id,
name: taxLine.description,
tax_line_id: taxLine.id,
}))
await applyTranslations({
localeCode: data.locale,
objects: translatedShippingMethodsTaxRates,
container,
})
await Promise.all([
orderModuleService.upsertOrderLineItemTaxLines(
translatedItemsTaxRates.map((taxRate) => ({
id: taxRate.tax_line_id,
description: taxRate.name,
}))
),
orderModuleService.upsertOrderShippingMethodTaxLines(
translatedShippingMethodsTaxRates.map((taxRate) => ({
id: taxRate.tax_line_id,
description: taxRate.name,
}))
),
])
return new StepResponse(void 0, [
originalItemTaxLines,
originalShippingMethodsTaxLines,
])
},
async (compensation, { container }) => {
if (!compensation?.length) {
return
}
const [originalItemTaxLines, originalShippingMethodsTaxLines] = compensation
const orderModuleService = container.resolve(Modules.ORDER)
await Promise.all([
orderModuleService.upsertOrderLineItemTaxLines(
originalItemTaxLines.map((taxLine) => ({
id: taxLine.id,
description: taxLine.description,
}))
),
orderModuleService.upsertOrderShippingMethodTaxLines(
originalShippingMethodsTaxLines.map((taxLine) => ({
id: taxLine.id,
description: taxLine.description,
}))
),
])
}
)

View File

@@ -29,6 +29,7 @@ import {
} from "../steps"
import { throwIfOrderIsCancelled } from "../utils/order-validation"
import { findOrCreateCustomerStep } from "../../cart"
import { updateOrderTaxLinesTranslationsStep } from "../steps/update-order-tax-lines-translations"
/**
* The data to validate the order update.
@@ -288,6 +289,10 @@ export const updateOrderWorkflow = createWorkflow(
updateOrderShippingMethodsTranslationsStep({
locale: input.locale!,
shippingMethods: order.shipping_methods,
}),
updateOrderTaxLinesTranslationsStep({
order_id: input.id,
locale: input.locale!,
})
)
})

View File

@@ -1,4 +1,8 @@
import type { OrderWorkflowDTO } from "@medusajs/framework/types"
import type {
ItemTaxLineDTO,
OrderWorkflowDTO,
ShippingTaxLineDTO,
} from "@medusajs/framework/types"
import {
createWorkflow,
transform,
@@ -9,11 +13,13 @@ import {
import { useQueryGraphStep } from "../../common"
import { getItemTaxLinesStep } from "../../tax/steps/get-item-tax-lines"
import { setOrderTaxLinesForItemsStep } from "../steps"
import { getTranslatedTaxLinesStep } from "../../common/steps/get-translated-tax-lines"
const completeOrderFields = [
"id",
"currency_code",
"email",
"locale",
"region.id",
"region.automatic_taxes",
"items.id",
@@ -65,6 +71,7 @@ const orderFields = [
"id",
"currency_code",
"email",
"locale",
"region.id",
"region.automatic_taxes",
"shipping_methods.tax_lines.id",
@@ -248,10 +255,17 @@ export const updateOrderTaxLinesWorkflow = createWorkflow(
)
)
const translatedTaxLines = getTranslatedTaxLinesStep({
itemTaxLines: taxLineItems.lineItemTaxLines,
shippingTaxLines: taxLineItems.shippingMethodsTaxLines,
locale: order.locale,
})
setOrderTaxLinesForItemsStep({
order,
item_tax_lines: taxLineItems.lineItemTaxLines,
shipping_tax_lines: taxLineItems.shippingMethodsTaxLines,
item_tax_lines: translatedTaxLines.itemTaxLines as ItemTaxLineDTO[],
shipping_tax_lines:
translatedTaxLines.shippingTaxLines as ShippingTaxLineDTO[],
})
return new WorkflowResponse({

View File

@@ -91,6 +91,7 @@ function normalizeTaxModuleContext(
},
customer,
is_return: isReturn ?? false,
locale: orderOrCart.locale,
shipping_methods: orderOrCart.shipping_methods?.map((method) => ({
id: method.id,
name: method.name,

View File

@@ -1136,7 +1136,7 @@ export interface OrderDTO {
/**
* The locale of the order.
*/
locale?: string | null
locale?: string
/**
* Holds custom data in key-value pairs.

View File

@@ -421,6 +421,10 @@ export interface TaxableShippingDTO {
* context is later passed to the underlying tax provider.
*/
export interface TaxCalculationContext {
/**
* The locale of the tax calculation.
*/
locale?: string
/**
* The customer's address
*/

View File

@@ -0,0 +1,49 @@
import { applyTranslations } from "./apply-translations"
import {
ItemTaxLineDTO,
MedusaContainer,
ShippingTaxLineDTO,
} from "@medusajs/types"
/**
* Applies translations to tax lines. If you are using a tax provider that doesn't have TaxRates defined in the database,
* you should apply the translations inside of your tax provider's `getTaxLines` method, using the `locale` provided in the context.
*
* @param taxLines - The tax lines to apply translations to.
* @param locale - The locale to apply translations to.
* @param container - The container to use for the translations.
* @returns The tax lines with translations applied.
*/
export const applyTranslationsToTaxLines = async (
taxLines: ItemTaxLineDTO[] | ShippingTaxLineDTO[],
locale: string | undefined,
container: MedusaContainer
) => {
const translatedTaxRates = taxLines.map(
(taxLine: ItemTaxLineDTO | ShippingTaxLineDTO) => ({
id: taxLine.rate_id,
name: taxLine.name,
})
)
await applyTranslations({
localeCode: locale,
objects: translatedTaxRates,
container,
})
const rateTranslationMap = new Map<string, string>()
for (const translatedRate of translatedTaxRates) {
if (!!translatedRate.id) {
rateTranslationMap.set(translatedRate.id, translatedRate.name)
}
}
for (const taxLine of taxLines) {
if (taxLine.rate_id) {
taxLine.name = rateTranslationMap.get(taxLine.rate_id)!
}
}
return taxLines
}

View File

@@ -1 +1,2 @@
export * from "./apply-translations"
export * from "./apply-translations-to-tax-lines"