diff --git a/.changeset/modern-dryers-fix.md b/.changeset/modern-dryers-fix.md new file mode 100644 index 0000000000..670da03c1b --- /dev/null +++ b/.changeset/modern-dryers-fix.md @@ -0,0 +1,8 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/types": patch +"@medusajs/utils": patch +"@medusajs/medusa": patch +--- + +feat(core-flows,types,utils,medusa): Translate tax lines diff --git a/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts b/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts index 5c79238e06..428b7afa26 100644 --- a/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts @@ -31,6 +31,8 @@ medusaIntegrationTestRunner({ let product: { id: string; variants: { id: string; title: string }[] } let salesChannel: { id: string } let shippingProfile: { id: string } + let taxRegion: { id: string } + let taxRate: { id: string; name: string } beforeAll(async () => { appContainer = getContainer() @@ -85,6 +87,59 @@ medusaIntegrationTestRunner({ ) ).data.region + // Create tax region and tax rate for tax line translations + taxRegion = ( + await api.post( + "/admin/tax-regions", + { + country_code: "us", + provider_id: "tp_system", + }, + adminHeaders + ) + ).data.tax_region + + // Create the default tax rate + taxRate = ( + await api.post( + "/admin/tax-rates", + { + tax_region_id: taxRegion.id, + name: "US Sales Tax", + rate: 5, + code: "US_SALES_TAX", + is_default: true, + }, + adminHeaders + ) + ).data.tax_rate + + // Create translations for tax rate name + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taxe de vente US", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "US Umsatzsteuer", + }, + }, + ], + }, + adminHeaders + ) + // Create product with description for translation product = ( await api.post( @@ -516,6 +571,255 @@ medusaIntegrationTestRunner({ }) }) + describe("Tax line translations", () => { + it("should translate tax line descriptions when creating a cart with locale", async () => { + const response = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "fr-FR", + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.cart.items[0]).toEqual( + expect.objectContaining({ + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + description: "Taxe de vente US", + rate: 5, + }), + ]), + }) + ) + }) + + it("should use original tax line description when no locale is provided", async () => { + const response = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.cart.items[0]).toEqual( + expect.objectContaining({ + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + description: "US Sales Tax", + rate: 5, + }), + ]), + }) + ) + }) + + it("should translate tax line descriptions when adding items to cart with locale", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "de-DE", + shipping_address: shippingAddressData, + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + const addItemResponse = await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + + expect(addItemResponse.status).toEqual(200) + expect(addItemResponse.data.cart.items[0]).toEqual( + expect.objectContaining({ + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + description: "US Umsatzsteuer", + rate: 5, + }), + ]), + }) + ) + }) + + it("should re-translate tax line descriptions when cart locale is updated", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "fr-FR", + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + // Verify French translation + expect(cart.items[0].tax_lines[0].description).toEqual( + "Taxe de vente US" + ) + + // Update locale to German + const updateResponse = await api.post( + `/store/carts/${cart.id}`, + { + locale: "de-DE", + }, + storeHeaders + ) + + expect(updateResponse.status).toEqual(200) + + // Fetch updated cart + const updatedCartResponse = await api.get( + `/store/carts/${cart.id}`, + storeHeaders + ) + + const updatedCart = updatedCartResponse.data.cart + + // Verify German translation + expect(updatedCart.items[0]).toEqual( + expect.objectContaining({ + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + description: "US Umsatzsteuer", + rate: 5, + }), + ]), + }) + ) + }) + + it("should revert to original tax line description when locale has no translation", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "fr-FR", + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + // Verify French translation exists + expect(cart.items[0].tax_lines[0].description).toEqual( + "Taxe de vente US" + ) + + // Update to locale without translation + const updateResponse = await api.post( + `/store/carts/${cart.id}`, + { + locale: "ja-JP", + }, + storeHeaders + ) + + expect(updateResponse.status).toEqual(200) + + // Fetch updated cart + const updatedCartResponse = await api.get( + `/store/carts/${cart.id}`, + storeHeaders + ) + + const updatedCart = updatedCartResponse.data.cart + + // Should revert to original description + expect(updatedCart.items[0]).toEqual( + expect.objectContaining({ + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + description: "US Sales Tax", + rate: 5, + }), + ]), + }) + ) + }) + + it("should translate tax lines for all items when multiple items are added", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "fr-FR", + shipping_address: shippingAddressData, + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + // Add first item + await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 1, + }, + storeHeaders + ) + + // Add second item + const response = await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[1].id, + quantity: 1, + }, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.cart.items).toHaveLength(2) + + // Both items should have translated tax lines + response.data.cart.items.forEach((item: any) => { + expect(item.tax_lines).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: "Taxe de vente US", + rate: 5, + }), + ]) + ) + }) + }) + }) + describe("POST /store/carts/:id/shipping-methods (shipping method translation)", () => { let shippingOption diff --git a/integration-tests/http/__tests__/claims/claims-translations.spec.ts b/integration-tests/http/__tests__/claims/claims-translations.spec.ts index 797dfa59c6..8584c715c8 100644 --- a/integration-tests/http/__tests__/claims/claims-translations.spec.ts +++ b/integration-tests/http/__tests__/claims/claims-translations.spec.ts @@ -1,7 +1,9 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { MedusaContainer } from "@medusajs/types" import { ClaimReason, ClaimType, + ContainerRegistrationKeys, Modules, ProductStatus, RuleOperator, @@ -10,6 +12,7 @@ import { adminHeaders, createAdminUser, } from "../../../helpers/create-admin-user" +import { setupTaxStructure } from "../../../modules/__tests__/fixtures" jest.setTimeout(60000) @@ -17,36 +20,34 @@ process.env.MEDUSA_FF_TRANSLATION = "true" medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { + let appContainer: MedusaContainer let order - let customer - let returnShippingOption - let outboundShippingOption - let shippingProfile - let fulfillmentSet - let inventoryItem - let location - let salesChannel - let region let product let productExtra + let shippingProfile + let stockLocation + let location + let fulfillmentSet + let inventoryItem + let inventoryItemExtra + let taxRate + let salesChannel + let region + let customer + let shippingOption + let returnShippingOption + let outboundShippingOption + const shippingProviderId = "manual_test-provider" beforeEach(async () => { - const container = getContainer() - await createAdminUser(dbConnection, adminHeaders, container) + appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) - customer = ( - await api.post( - "/admin/customers", - { - first_name: "joe", - email: "joe@admin.com", - }, - adminHeaders - ) - ).data.customer + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) - // Set up supported locales in the store - const storeModule = container.resolve(Modules.STORE) + const storeModule = appContainer.resolve(Modules.STORE) const [defaultStore] = await storeModule.listStores( {}, { select: ["id"], take: 1 } @@ -59,10 +60,34 @@ medusaIntegrationTestRunner({ ], }) + region = ( + await api.post( + "/admin/regions", + { + name: "test-region", + currency_code: "usd", + }, + adminHeaders + ) + ).data.region + + customer = ( + await api.post( + "/admin/customers", + { + first_name: "joe", + email: "joe@admin.com", + }, + adminHeaders + ) + ).data.customer + salesChannel = ( await api.post( "/admin/sales-channels", - { name: "Webshop", description: "channel" }, + { + name: "Test channel", + }, adminHeaders ) ).data.sales_channel @@ -70,21 +95,13 @@ medusaIntegrationTestRunner({ shippingProfile = ( await api.post( `/admin/shipping-profiles`, - { name: "Test", type: "default" }, - adminHeaders - ) - ).data.shipping_profile - - region = ( - await api.post( - "/admin/regions", { - name: "Test Region", - currency_code: "usd", + name: "Test", + type: "default", }, adminHeaders ) - ).data.region + ).data.shipping_profile location = ( await api.post( @@ -154,7 +171,6 @@ medusaIntegrationTestRunner({ title: "Extra variant", sku: "extra-variant", options: { size: "large" }, - manage_inventory: false, prices: [{ currency_code: "usd", amount: 50 }], }, ], @@ -163,7 +179,7 @@ medusaIntegrationTestRunner({ ) ).data.product - location = ( + stockLocation = ( await api.post( `/admin/stock-locations/${location.id}/fulfillment-sets?fields=*fulfillment_sets`, { name: "Test", type: "test-type" }, @@ -173,7 +189,7 @@ medusaIntegrationTestRunner({ fulfillmentSet = ( await api.post( - `/admin/fulfillment-sets/${location.fulfillment_sets[0].id}/service-zones`, + `/admin/fulfillment-sets/${stockLocation.fulfillment_sets[0].id}/service-zones`, { name: "Test", geo_zones: [{ type: "country", country_code: "us" }], @@ -182,12 +198,55 @@ medusaIntegrationTestRunner({ ) ).data.fulfillment_set + inventoryItemExtra = ( + await api.get(`/admin/inventory-items?sku=extra-variant`, adminHeaders) + ).data.inventory_items[0] + await api.post( - `/admin/stock-locations/${location.id}/fulfillment-providers`, - { add: ["manual_test-provider"] }, + `/admin/inventory-items/${inventoryItemExtra.id}/location-levels`, + { + location_id: location.id, + stocked_quantity: 10, + }, adminHeaders ) + const remoteLink = appContainer.resolve( + ContainerRegistrationKeys.REMOTE_LINK + ) + + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: [shippingProviderId] }, + adminHeaders + ) + + shippingOption = ( + await api.post( + "/admin/shipping-options", + { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: shippingProviderId, + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + currency_code: "usd", + amount: 10, + }, + ], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + returnShippingOption = ( await api.post( "/admin/shipping-options", @@ -222,7 +281,7 @@ medusaIntegrationTestRunner({ name: "Outbound shipping", service_zone_id: fulfillmentSet.service_zones[0].id, shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", + provider_id: shippingProviderId, price_type: "flat", type: { label: "Test type", @@ -242,11 +301,78 @@ medusaIntegrationTestRunner({ ) ).data.shipping_option - // Create translations for shipping options + await remoteLink.create([ + { + [Modules.STOCK_LOCATION]: { + stock_location_id: stockLocation.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_provider_id: shippingProviderId, + }, + }, + { + [Modules.STOCK_LOCATION]: { + stock_location_id: stockLocation.id, + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: fulfillmentSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: stockLocation.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItem.id, + }, + }, + { + [Modules.PRODUCT]: { + variant_id: productExtra.variants[0].id, + }, + [Modules.INVENTORY]: { + inventory_item_id: inventoryItemExtra.id, + }, + }, + ]) + + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { code: string }) => rate.code === "CADEFAULT" + ) + + // Create translations for tax rates and shipping options await api.post( "/admin/translations/batch", { create: [ + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, { reference_id: returnShippingOption.id, reference: "shipping_option", @@ -278,9 +404,8 @@ medusaIntegrationTestRunner({ }) const createOrderWithLocale = async (locale?: string) => { - const container = getContainer() - const orderModule = container.resolve(Modules.ORDER) - const inventoryModule = container.resolve(Modules.INVENTORY) + const orderModule = appContainer.resolve(Modules.ORDER) + const inventoryModule = appContainer.resolve(Modules.INVENTORY) const createdOrder = await orderModule.createOrders({ region_id: region.id, @@ -302,6 +427,7 @@ medusaIntegrationTestRunner({ address_1: "Test", city: "Test", country_code: "US", + province: "CA", postal_code: "12345", phone: "12345", }, @@ -312,6 +438,7 @@ medusaIntegrationTestRunner({ address_1: "Test", city: "Test", country_code: "US", + province: "CA", postal_code: "12345", }, shipping_methods: [ @@ -319,6 +446,7 @@ medusaIntegrationTestRunner({ name: "Test shipping method", amount: 10, data: {}, + shipping_option_id: shippingOption.id, }, ], currency_code: "usd", @@ -334,7 +462,7 @@ medusaIntegrationTestRunner({ await inventoryModule.createReservationItems([ { inventory_item_id: inventoryItem.id, - location_id: location.id, + location_id: stockLocation.id, quantity: 2, line_item_id: createdOrder.items![0].id, }, @@ -357,6 +485,288 @@ medusaIntegrationTestRunner({ return createdOrder } + describe("Claims tax line translations", () => { + describe("when adding outbound items to a claim", () => { + it("should translate tax lines based on order locale when adding new items", async () => { + order = await createOrderWithLocale("fr-FR") + + const claim = ( + await api.post( + "/admin/claims", + { + order_id: order.id, + type: ClaimType.REPLACE, + description: "Test claim", + }, + adminHeaders + ) + ).data.claim + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const newItem = updatedOrder.items.find( + (item) => item.title === "Extra product" + ) + + expect(newItem).toBeDefined() + expect(newItem.tax_lines).toBeDefined() + expect(newItem.tax_lines.length).toBeGreaterThan(0) + + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine).toBeDefined() + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + + it("should use original tax line description when order has no locale", async () => { + order = await createOrderWithLocale() + + const claim = ( + await api.post( + "/admin/claims", + { + order_id: order.id, + type: ClaimType.REPLACE, + description: "Test claim", + }, + adminHeaders + ) + ).data.claim + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const newItem = updatedOrder.items.find( + (item) => item.title === "Extra product" + ) + + expect(newItem).toBeDefined() + expect(newItem.tax_lines).toBeDefined() + expect(newItem.tax_lines.length).toBeGreaterThan(0) + + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine).toBeDefined() + expect(taxLine.description).toEqual("CA Default Rate") + }) + }) + + describe("when adding outbound shipping methods to a claim", () => { + it("should translate shipping method tax lines based on order locale", async () => { + order = await createOrderWithLocale("fr-FR") + + await api.post( + `/admin/orders/${order.id}`, + { locale: "fr-FR" }, + adminHeaders + ) + + const claim = ( + await api.post( + "/admin/claims", + { + order_id: order.id, + type: ClaimType.REPLACE, + description: "Test claim", + }, + adminHeaders + ) + ).data.claim + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/outbound/shipping-method`, + { shipping_option_id: outboundShippingOption.id }, + adminHeaders + ) + + await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = outboundShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + + it("should use original tax line description when order has no locale", async () => { + order = await createOrderWithLocale() + + const claim = ( + await api.post( + "/admin/claims", + { + order_id: order.id, + type: ClaimType.REPLACE, + description: "Test claim", + }, + adminHeaders + ) + ).data.claim + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/outbound/shipping-method`, + { shipping_option_id: outboundShippingOption.id }, + adminHeaders + ) + + await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = outboundShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("CA Default Rate") + }) + }) + + describe("when updating outbound items in a claim", () => { + it("should preserve tax line translations when updating item quantity", async () => { + order = await createOrderWithLocale("fr-FR") + + await api.post( + `/admin/orders/${order.id}`, + { locale: "fr-FR" }, + adminHeaders + ) + + const claim = ( + await api.post( + "/admin/claims", + { + order_id: order.id, + type: ClaimType.REPLACE, + description: "Test claim", + }, + adminHeaders + ) + ).data.claim + + const addItemResponse = await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const actionId = addItemResponse.data.order_preview.items.find( + (item) => + !!item.actions?.find((action) => action.action === "ITEM_ADD") + ).actions[0].id + + await api.post( + `/admin/claims/${claim.id}/outbound/items/${actionId}`, + { + quantity: 2, + }, + adminHeaders + ) + + await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const newItem = updatedOrder.items.find( + (item) => item.title === "Extra product" + ) + + expect(newItem).toBeDefined() + expect(newItem.tax_lines).toBeDefined() + expect(newItem.tax_lines.length).toBeGreaterThan(0) + + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine).toBeDefined() + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + }) + }) + describe("Claim shipping method translation", () => { describe("Inbound (return) shipping method", () => { it("should translate inbound shipping method name using French locale", async () => { diff --git a/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts b/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts index a3718d6f10..5a2cd4c8a2 100644 --- a/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts +++ b/integration-tests/http/__tests__/draft-order/admin/draft-order-translation.spec.ts @@ -21,13 +21,16 @@ medusaIntegrationTestRunner({ let shippingProfile: { id: string } let stockLocation: { id: string } let shippingOption: { id: string } + let taxRate: { id: string } beforeAll(async () => { appContainer = getContainer() }) beforeEach(async () => { - await setupTaxStructure(appContainer.resolve(Modules.TAX)) + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) await createAdminUser(dbConnection, adminHeaders, appContainer) salesChannel = ( @@ -162,6 +165,14 @@ medusaIntegrationTestRunner({ ) ).data.shipping_option + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { code: string }) => rate.code === "CADEFAULT" + ) + await api.post( "/admin/translations/batch", { @@ -224,6 +235,22 @@ medusaIntegrationTestRunner({ name: "Test-Versandoption", }, }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, ], }, adminHeaders @@ -244,6 +271,7 @@ medusaIntegrationTestRunner({ address_1: "123 Main St", city: "Anytown", country_code: "us", + province: "ca", postal_code: "12345", first_name: "John", }, @@ -283,6 +311,12 @@ medusaIntegrationTestRunner({ variant_title: "Petit", }) ) + + expect(updatedDraftOrder.items[0].tax_lines.length).toBeGreaterThan(0) + const taxLine = updatedDraftOrder.items[0].tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") }) it("should have original values when draft order has no locale", async () => { @@ -297,6 +331,7 @@ medusaIntegrationTestRunner({ address_1: "123 Main St", city: "Anytown", country_code: "us", + province: "ca", postal_code: "12345", first_name: "John", }, @@ -336,6 +371,12 @@ medusaIntegrationTestRunner({ variant_title: "Small", }) ) + + expect(updatedDraftOrder.items[0].tax_lines.length).toBeGreaterThan(0) + const taxLine = updatedDraftOrder.items[0].tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("CA Default Rate") }) it("should translate multiple items added to draft order", async () => { @@ -351,6 +392,7 @@ medusaIntegrationTestRunner({ address_1: "123 Main St", city: "Anytown", country_code: "us", + province: "ca", postal_code: "12345", first_name: "John", }, @@ -407,6 +449,146 @@ medusaIntegrationTestRunner({ variant_title: "Mittel", }) ) + + expect(smallItem.tax_lines.length).toBeGreaterThan(0) + const smallTaxLine = smallItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(smallTaxLine.description).toEqual("CA Standardsteuersatz") + + expect(mediumItem.tax_lines.length).toBeGreaterThan(0) + const mediumTaxLine = mediumItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(mediumTaxLine.description).toEqual("CA Standardsteuersatz") + }) + }) + + describe("POST /admin/draft-orders/:id/edit/shipping-methods (add shipping method to draft order)", () => { + it("should translate shipping method tax lines when adding to draft order with locale", async () => { + const draftOrder = ( + await api.post( + "/admin/draft-orders", + { + email: "test@test.com", + region_id: region.id, + sales_channel_id: salesChannel.id, + locale: "fr-FR", + shipping_address: { + address_1: "123 Main St", + city: "Anytown", + country_code: "us", + province: "ca", + postal_code: "12345", + first_name: "John", + }, + }, + adminHeaders + ) + ).data.draft_order + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit`, + {}, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/items`, + { + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/shipping-methods`, + { + shipping_option_id: shippingOption.id, + }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + + const updatedDraftOrder = ( + await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders) + ).data.draft_order + + expect(updatedDraftOrder.shipping_methods.length).toBeGreaterThan(0) + + const shippingMethod = updatedDraftOrder.shipping_methods[0] + const taxLine = shippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + + it("should use original tax line description when draft order has no locale", async () => { + const draftOrder = ( + await api.post( + "/admin/draft-orders", + { + email: "test@test.com", + region_id: region.id, + sales_channel_id: salesChannel.id, + shipping_address: { + address_1: "123 Main St", + city: "Anytown", + country_code: "us", + province: "ca", + postal_code: "12345", + first_name: "John", + }, + }, + adminHeaders + ) + ).data.draft_order + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit`, + {}, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/items`, + { + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/shipping-methods`, + { + shipping_option_id: shippingOption.id, + }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + + const updatedDraftOrder = ( + await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders) + ).data.draft_order + + expect(updatedDraftOrder.shipping_methods.length).toBeGreaterThan(0) + + const shippingMethod = updatedDraftOrder.shipping_methods[0] + const taxLine = shippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine).toBeDefined() + expect(taxLine.description).toEqual("CA Default Rate") }) }) @@ -424,6 +606,7 @@ medusaIntegrationTestRunner({ address_1: "123 Main St", city: "Anytown", country_code: "us", + province: "ca", postal_code: "12345", first_name: "John", }, @@ -449,6 +632,14 @@ medusaIntegrationTestRunner({ adminHeaders ) + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/shipping-methods`, + { + shipping_option_id: shippingOption.id, + }, + adminHeaders + ) + await api.post( `/admin/draft-orders/${draftOrder.id}/edit/confirm`, {}, @@ -464,6 +655,12 @@ medusaIntegrationTestRunner({ ) expect(frenchSmallItem.variant_title).toEqual("Petit") + expect(frenchSmallItem.tax_lines.length).toBeGreaterThan(0) + const frenchTaxLine = frenchSmallItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(frenchTaxLine.description).toEqual("Taux par défaut CA") + await api.post( `/admin/draft-orders/${draftOrder.id}`, { locale: "de-DE" }, @@ -494,6 +691,31 @@ medusaIntegrationTestRunner({ variant_title: "Mittel", }) ) + + expect(germanSmallItem.tax_lines.length).toBeGreaterThan(0) + const germanSmallTaxLine = germanSmallItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(germanSmallTaxLine.description).toEqual( + "CA Standardsteuersatz" + ) + + expect(germanMediumItem.tax_lines.length).toBeGreaterThan(0) + const germanMediumTaxLine = germanMediumItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(germanMediumTaxLine.description).toEqual( + "CA Standardsteuersatz" + ) + + expect(updatedDraftOrder.shipping_methods.length).toBeGreaterThan(0) + + const shippingMethod = updatedDraftOrder.shipping_methods[0] + const shippingTaxLine = shippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(shippingTaxLine).toBeDefined() + expect(shippingTaxLine.description).toEqual("CA Standardsteuersatz") }) }) diff --git a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts index d020afdc2f..87cab294a0 100644 --- a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts @@ -36,13 +36,16 @@ medusaIntegrationTestRunner({ let outboundShippingOption: { id: string } let returnShippingOption: { id: string } let inventoryItem: { id: string } + let taxRate: { id: string } beforeAll(async () => { appContainer = getContainer() }) beforeEach(async () => { - await setupTaxStructure(appContainer.resolve(Modules.TAX)) + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) await createAdminUser(dbConnection, adminHeaders, appContainer) const publishableKey = await generatePublishableKey(appContainer) storeHeaders = generateStoreHeaders({ publishableKey }) @@ -251,6 +254,13 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data.shipping_option + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { code: string }) => rate.code === "CADEFAULT" + ) await api.post( "/admin/translations/batch", @@ -330,6 +340,22 @@ medusaIntegrationTestRunner({ name: "Rückversand", }, }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, ], }, adminHeaders @@ -447,73 +473,21 @@ medusaIntegrationTestRunner({ variant_title: "Moyen", }) ) - }) - it("should translate exchange items using German locale", async () => { - const order = await createOrderFromCart("de-DE") - - await api.post( - `/admin/orders/${order.id}/fulfillments`, - { - location_id: stockLocation.id, - items: [{ id: order.items[0].id, quantity: 1 }], - }, - adminHeaders + expect(newItem.tax_lines.length).toBeGreaterThan(0) + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" ) + expect(taxLine.description).toEqual("Taux par défaut CA") - const exchange = ( - await api.post( - "/admin/exchanges", - { order_id: order.id, description: "Test exchange" }, - adminHeaders - ) - ).data.exchange - - // Add inbound item (item being returned) - await api.post( - `/admin/exchanges/${exchange.id}/inbound/items`, - { - items: [{ id: order.items[0].id, quantity: 1 }], - }, - adminHeaders + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === outboundShippingOption.id ) - - // Add outbound item (new item being sent) - await api.post( - `/admin/exchanges/${exchange.id}/outbound/items`, - { - items: [{ variant_id: product.variants[1].id, quantity: 1 }], - }, - adminHeaders - ) - - await api.post( - `/admin/exchanges/${exchange.id}/outbound/shipping-method`, - { shipping_option_id: outboundShippingOption.id }, - adminHeaders - ) - - await api.post( - `/admin/exchanges/${exchange.id}/request`, - {}, - adminHeaders - ) - - const updatedOrder = ( - await api.get(`/admin/orders/${order.id}`, adminHeaders) - ).data.order - - const newItem = updatedOrder.items.find( - (item: any) => item.variant_id === product.variants[1].id - ) - - expect(newItem).toEqual( - expect.objectContaining({ - product_title: "Medusa T-Shirt DE", - product_description: "Ein bequemes Baumwoll-T-Shirt", - variant_title: "Mittel", - }) + expect(outboundShippingMethod.tax_lines.length).toBeGreaterThan(0) + const shippingTaxLine = outboundShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" ) + expect(shippingTaxLine.description).toEqual("Taux par défaut CA") }) it("should have original values when order has no locale", async () => { @@ -580,6 +554,21 @@ medusaIntegrationTestRunner({ variant_title: "Medium", }) ) + + expect(newItem.tax_lines.length).toBeGreaterThan(0) + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("CA Default Rate") + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === outboundShippingOption.id + ) + expect(outboundShippingMethod.tax_lines.length).toBeGreaterThan(0) + const shippingTaxLine = outboundShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(shippingTaxLine.description).toEqual("CA Default Rate") }) }) diff --git a/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts b/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts index 6a07ba277c..a1a35a4e69 100644 --- a/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts +++ b/integration-tests/http/__tests__/order-edits/order-edit-translation.spec.ts @@ -35,13 +35,16 @@ medusaIntegrationTestRunner({ let shippingOption: { id: string } let additionalShippingOption: { id: string } let inventoryItem: { id: string } + let taxRate: { id: string } beforeAll(async () => { appContainer = getContainer() }) beforeEach(async () => { - await setupTaxStructure(appContainer.resolve(Modules.TAX)) + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) await createAdminUser(dbConnection, adminHeaders, appContainer) const publishableKey = await generatePublishableKey(appContainer) storeHeaders = generateStoreHeaders({ publishableKey }) @@ -217,6 +220,13 @@ medusaIntegrationTestRunner({ adminHeaders ) ).data.shipping_option + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { code: string }) => rate.code === "CADEFAULT" + ) await api.post( "/admin/translations/batch", @@ -296,6 +306,22 @@ medusaIntegrationTestRunner({ name: "Zusätzliche Versandoption", }, }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, ], }, adminHeaders @@ -386,6 +412,12 @@ medusaIntegrationTestRunner({ variant_title: "Moyen", }) ) + + expect(newItem.tax_lines.length).toBeGreaterThan(0) + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") }) it("should have original values when order has no locale", async () => { @@ -426,46 +458,12 @@ medusaIntegrationTestRunner({ variant_title: "Medium", }) ) - }) - it("should translate items using German locale", async () => { - const order = await createOrderFromCart("de-DE") - - await api.post( - "/admin/order-edits", - { order_id: order.id }, - adminHeaders - ) - - await api.post( - `/admin/order-edits/${order.id}/items`, - { - items: [{ variant_id: product.variants[1].id, quantity: 1 }], - }, - adminHeaders - ) - - await api.post( - `/admin/order-edits/${order.id}/confirm`, - {}, - adminHeaders - ) - - const updatedOrder = ( - await api.get(`/admin/orders/${order.id}`, adminHeaders) - ).data.order - - const newItem = updatedOrder.items.find( - (item) => item.variant_id === product.variants[1].id - ) - - expect(newItem).toEqual( - expect.objectContaining({ - product_title: "Medusa T-Shirt DE", - product_description: "Ein bequemes Baumwoll-T-Shirt", - variant_title: "Mittel", - }) + expect(newItem.tax_lines.length).toBeGreaterThan(0) + const taxLine = newItem.tax_lines.find( + (tl) => tl.code === "CADEFAULT" ) + expect(taxLine.description).toEqual("CA Default Rate") }) }) @@ -557,6 +555,78 @@ medusaIntegrationTestRunner({ ]) ) }) + + it("should translate shipping method tax lines when adding to order edit with locale", async () => { + const order = await createOrderFromCart("fr-FR") + + await api.post( + "/admin/order-edits", + { order_id: order.id }, + adminHeaders + ) + + await api.post( + `/admin/order-edits/${order.id}/shipping-method`, + { shipping_option_id: shippingOption.id }, + adminHeaders + ) + + await api.post( + `/admin/order-edits/${order.id}/confirm`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const newShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === shippingOption.id + ) + + expect(newShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = newShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + + it("should use original tax line description when order has no locale", async () => { + const order = await createOrderFromCart() + + await api.post( + "/admin/order-edits", + { order_id: order.id }, + adminHeaders + ) + + await api.post( + `/admin/order-edits/${order.id}/shipping-method`, + { shipping_option_id: shippingOption.id }, + adminHeaders + ) + + await api.post( + `/admin/order-edits/${order.id}/confirm`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const newShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === shippingOption.id + ) + + expect(newShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = newShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("CA Default Rate") + }) }) }) }, diff --git a/integration-tests/http/__tests__/order/admin/order-translation.spec.ts b/integration-tests/http/__tests__/order/admin/order-translation.spec.ts index c779a64a74..260f8dcd8f 100644 --- a/integration-tests/http/__tests__/order/admin/order-translation.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order-translation.spec.ts @@ -36,13 +36,16 @@ medusaIntegrationTestRunner({ let returnShippingOption: { id: string } let outboundShippingOption: { id: string } let inventoryItem: { id: string } + let taxRate: { id: string; name: string } beforeAll(async () => { appContainer = getContainer() }) beforeEach(async () => { - await setupTaxStructure(appContainer.resolve(Modules.TAX)) + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) await createAdminUser(dbConnection, adminHeaders, appContainer) const publishableKey = await generatePublishableKey(appContainer) storeHeaders = generateStoreHeaders({ publishableKey }) @@ -255,7 +258,16 @@ medusaIntegrationTestRunner({ ) ).data.shipping_option - // Create translations for product, variants, and shipping options + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { is_default: boolean; code: string }) => + rate.is_default && rate.code === "CADEFAULT" + ) + + // Create translations for product and variants await api.post( "/admin/translations/batch", { @@ -318,6 +330,22 @@ medusaIntegrationTestRunner({ name: "Test-Versandoption", }, }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, ], }, adminHeaders @@ -521,6 +549,119 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("Tax line translations", () => { + it("should translate tax line descriptions when order locale is updated", async () => { + // Create order with French locale + const order = await createOrderFromCart("fr-FR") + + // Verify tax lines exist and have French translation + expect(order.items[0].tax_lines).toBeDefined() + expect(order.items[0].tax_lines.length).toBe(1) + expect(order.items[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "Taux par défaut CA", + rate: 5, + code: "CADEFAULT", + }) + ) + + // Update order locale to German + await api.post( + `/admin/orders/${order.id}`, + { locale: "de-DE" }, + adminHeaders + ) + + // Fetch updated order + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + // Verify tax lines are translated to German + expect(updatedOrder.items[0].tax_lines.length).toBe(1) + expect(updatedOrder.items[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "CA Standardsteuersatz", + rate: 5, + code: "CADEFAULT", + }) + ) + }) + + it("should preserve tax line translations when order is created with locale", async () => { + // Create order with French locale + const order = await createOrderFromCart("fr-FR") + + // Verify tax lines are translated from the start + expect(order.items[0].tax_lines).toBeDefined() + expect(order.items[0].tax_lines.length).toBeGreaterThan(0) + expect(order.items[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "Taux par défaut CA", + rate: 5, + code: "CADEFAULT", + }) + ) + }) + + it("should use original tax line description when order has no locale", async () => { + // Create order without locale + const order = await createOrderFromCart() + + // Verify tax lines use original description + expect(order.items[0].tax_lines).toBeDefined() + expect(order.items[0].tax_lines.length).toBeGreaterThan(0) + expect(order.items[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "CA Default Rate", + rate: 5, + code: "CADEFAULT", + }) + ) + }) + + it("should translate shipping method tax lines when order locale is updated", async () => { + // Create order with French locale + const order = await createOrderFromCart("fr-FR") + + // Verify shipping method tax lines exist and are translated + expect(order.shipping_methods).toBeDefined() + expect(order.shipping_methods.length).toBeGreaterThan(0) + if (order.shipping_methods[0].tax_lines?.length > 0) { + expect(order.shipping_methods[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "Taux par défaut CA", + rate: 5, + code: "CADEFAULT", + }) + ) + + // Update order locale to German + await api.post( + `/admin/orders/${order.id}`, + { locale: "de-DE" }, + adminHeaders + ) + + // Fetch updated order + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + // Verify shipping method tax lines are translated + if (updatedOrder.shipping_methods[0].tax_lines?.length > 0) { + expect(updatedOrder.shipping_methods[0].tax_lines[0]).toEqual( + expect.objectContaining({ + description: "CA Standardsteuersatz", + rate: 5, + code: "CADEFAULT", + }) + ) + } + } + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/returns/returns-translations.spec.ts b/integration-tests/http/__tests__/returns/returns-translations.spec.ts new file mode 100644 index 0000000000..5d6545cefa --- /dev/null +++ b/integration-tests/http/__tests__/returns/returns-translations.spec.ts @@ -0,0 +1,428 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { MedusaContainer } from "@medusajs/types" +import { Modules, ProductStatus, RuleOperator } from "@medusajs/utils" +import { + adminHeaders, + createAdminUser, + generatePublishableKey, + generateStoreHeaders, +} from "../../../helpers/create-admin-user" +import { setupTaxStructure } from "../../../modules/__tests__/fixtures" + +jest.setTimeout(300000) + +process.env.MEDUSA_FF_TRANSLATION = "true" + +const shippingAddressData = { + address_1: "test address 1", + address_2: "test address 2", + city: "SF", + country_code: "us", + province: "CA", + postal_code: "94016", +} + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Return Translation API", () => { + let appContainer: MedusaContainer + let storeHeaders: { headers: { [key: string]: string } } + let region: { id: string } + let product: { id: string; variants: { id: string; title: string }[] } + let salesChannel: { id: string } + let shippingProfile: { id: string } + let stockLocation: { id: string } + let shippingOption: { id: string } + let returnShippingOption: { id: string } + let inventoryItem: { id: string } + let taxRate: { id: string } + + beforeAll(async () => { + appContainer = getContainer() + }) + + beforeEach(async () => { + const taxStructure = await setupTaxStructure( + appContainer.resolve(Modules.TAX) + ) + await createAdminUser(dbConnection, adminHeaders, appContainer) + const publishableKey = await generatePublishableKey(appContainer) + storeHeaders = generateStoreHeaders({ publishableKey }) + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "Webshop", description: "channel" }, + adminHeaders + ) + ).data.sales_channel + + const storeModule = appContainer.resolve(Modules.STORE) + const [defaultStore] = await storeModule.listStores( + {}, + { select: ["id"], take: 1 } + ) + await storeModule.updateStores(defaultStore.id, { + supported_locales: [ + { locale_code: "en-US" }, + { locale_code: "fr-FR" }, + { locale_code: "de-DE" }, + ], + }) + + region = ( + await api.post( + "/admin/regions", + { name: "US", currency_code: "usd", countries: ["us"] }, + adminHeaders + ) + ).data.region + + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: "default", type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location + + inventoryItem = ( + await api.post( + `/admin/inventory-items`, + { sku: "test-variant" }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItem.id}/location-levels`, + { location_id: stockLocation.id, stocked_quantity: 100 }, + adminHeaders + ) + + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + product = ( + await api.post( + "/admin/products", + { + title: "Medusa T-Shirt", + description: "A comfortable cotton t-shirt", + handle: "t-shirt", + status: ProductStatus.PUBLISHED, + shipping_profile_id: shippingProfile.id, + options: [{ title: "Size", values: ["S", "M"] }], + variants: [ + { + title: "Small", + sku: "SHIRT-S", + options: { Size: "S" }, + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 1, + }, + ], + prices: [{ amount: 1500, currency_code: "usd" }], + }, + { + title: "Medium", + sku: "SHIRT-M", + options: { Size: "M" }, + manage_inventory: false, + prices: [{ amount: 1500, currency_code: "usd" }], + }, + ], + }, + adminHeaders + ) + ).data.product + + const variantSmall = product.variants.find((v) => v.title === "Small") + const variantMedium = product.variants.find((v) => v.title === "Medium") + product.variants = [variantSmall!, variantMedium!] + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { name: "Test", type: "test-type" }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + const fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [{ currency_code: "usd", amount: 1000 }], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + + returnShippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: "Return shipping", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [{ currency_code: "usd", amount: 1000 }], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + const taxRatesResponse = await api.get( + `/admin/tax-rates?tax_region_id=${taxStructure.us.children.cal.province.id}`, + adminHeaders + ) + taxRate = taxRatesResponse.data.tax_rates.find( + (rate: { code: string }) => rate.code === "CADEFAULT" + ) + + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "fr-FR", + translations: { + name: "Taux par défaut CA", + }, + }, + { + reference_id: taxRate.id, + reference: "tax_rate", + locale_code: "de-DE", + translations: { + name: "CA Standardsteuersatz", + }, + }, + ], + }, + adminHeaders + ) + }) + + const createOrderFromCart = async (locale?: string) => { + const cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + email: "test@example.com", + region_id: region.id, + sales_channel_id: salesChannel.id, + locale, + shipping_address: shippingAddressData, + billing_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + ).data.cart + + await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + const paymentCollection = ( + await api.post( + `/store/payment-collections`, + { cart_id: cart.id }, + storeHeaders + ) + ).data.payment_collection + + await api.post( + `/store/payment-collections/${paymentCollection.id}/payment-sessions`, + { provider_id: "pp_system_default" }, + storeHeaders + ) + + const order = ( + await api.post(`/store/carts/${cart.id}/complete`, {}, storeHeaders) + ).data.order + + const fullOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + await api.post( + `/admin/orders/${fullOrder.id}/fulfillments`, + { + location_id: stockLocation.id, + items: [{ id: fullOrder.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + return (await api.get(`/admin/orders/${fullOrder.id}`, adminHeaders)) + .data.order + } + + describe("Return shipping method tax line translations", () => { + it("should translate shipping method tax lines based on order locale", async () => { + const order = await createOrderFromCart("fr-FR") + + const return_ = ( + await api.post( + "/admin/returns", + { + order_id: order.id, + description: "Test return", + }, + adminHeaders + ) + ).data.return + + await api.post( + `/admin/returns/${return_.id}/request-items`, + { + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/returns/${return_.id}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + await api.post( + `/admin/returns/${return_.id}/request`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const returnShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = returnShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("Taux par défaut CA") + }) + + it("should use original tax line description when order has no locale", async () => { + const order = await createOrderFromCart() + + const return_ = ( + await api.post( + "/admin/returns", + { + order_id: order.id, + description: "Test return", + }, + adminHeaders + ) + ).data.return + + await api.post( + `/admin/returns/${return_.id}/request-items`, + { + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/returns/${return_.id}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + await api.post( + `/admin/returns/${return_.id}/request`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const returnShippingMethod = updatedOrder.shipping_methods.find( + (sm) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod.tax_lines.length).toBeGreaterThan(0) + const taxLine = returnShippingMethod.tax_lines.find( + (tl) => tl.code === "CADEFAULT" + ) + expect(taxLine.description).toEqual("CA Default Rate") + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/cart/workflows/update-tax-lines.ts b/packages/core/core-flows/src/cart/workflows/update-tax-lines.ts index afdddb990c..06ba6f9903 100644 --- a/packages/core/core-flows/src/cart/workflows/update-tax-lines.ts +++ b/packages/core/core-flows/src/cart/workflows/update-tax-lines.ts @@ -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({ 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 index 2778c08aaa..f1829a8603 100644 --- a/packages/core/core-flows/src/cart/workflows/upsert-tax-lines.ts +++ b/packages/core/core-flows/src/cart/workflows/upsert-tax-lines.ts @@ -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[], }) } ) diff --git a/packages/core/core-flows/src/common/steps/get-translated-tax-lines.ts b/packages/core/core-flows/src/common/steps/get-translated-tax-lines.ts new file mode 100644 index 0000000000..8017fca926 --- /dev/null +++ b/packages/core/core-flows/src/common/steps/get-translated-tax-lines.ts @@ -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, + }) + } +) diff --git a/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts b/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts index f1516e696c..494100073b 100644 --- a/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts +++ b/packages/core/core-flows/src/draft-order/workflows/update-draft-order.ts @@ -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!, diff --git a/packages/core/core-flows/src/order/steps/update-order-tax-lines-translations.ts b/packages/core/core-flows/src/order/steps/update-order-tax-lines-translations.ts new file mode 100644 index 0000000000..29dc66349c --- /dev/null +++ b/packages/core/core-flows/src/order/steps/update-order-tax-lines-translations.ts @@ -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, + })) + ), + ]) + } +) diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts index 7a4dd38e45..3fe73cf8eb 100644 --- a/packages/core/core-flows/src/order/workflows/update-order.ts +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -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!, }) ) }) diff --git a/packages/core/core-flows/src/order/workflows/update-tax-lines.ts b/packages/core/core-flows/src/order/workflows/update-tax-lines.ts index 1e726d79f4..2efc000797 100644 --- a/packages/core/core-flows/src/order/workflows/update-tax-lines.ts +++ b/packages/core/core-flows/src/order/workflows/update-tax-lines.ts @@ -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({ 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 d1c9b583b6..6a767a211d 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 @@ -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, diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 53b2529600..769f6864fb 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -1136,7 +1136,7 @@ export interface OrderDTO { /** * The locale of the order. */ - locale?: string | null + locale?: string /** * Holds custom data in key-value pairs. diff --git a/packages/core/types/src/tax/common.ts b/packages/core/types/src/tax/common.ts index 20ea094de3..5aec26dd41 100644 --- a/packages/core/types/src/tax/common.ts +++ b/packages/core/types/src/tax/common.ts @@ -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 */ diff --git a/packages/core/utils/src/translations/apply-translations-to-tax-lines.ts b/packages/core/utils/src/translations/apply-translations-to-tax-lines.ts new file mode 100644 index 0000000000..c0ef890d15 --- /dev/null +++ b/packages/core/utils/src/translations/apply-translations-to-tax-lines.ts @@ -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() + 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 +} diff --git a/packages/core/utils/src/translations/index.ts b/packages/core/utils/src/translations/index.ts index aef62a261b..1e5fec3952 100644 --- a/packages/core/utils/src/translations/index.ts +++ b/packages/core/utils/src/translations/index.ts @@ -1 +1,2 @@ export * from "./apply-translations" +export * from "./apply-translations-to-tax-lines" diff --git a/packages/medusa/src/api/store/product-variants/helpers.ts b/packages/medusa/src/api/store/product-variants/helpers.ts index 5b796d8384..f89f33b822 100644 --- a/packages/medusa/src/api/store/product-variants/helpers.ts +++ b/packages/medusa/src/api/store/product-variants/helpers.ts @@ -3,8 +3,14 @@ import { ItemTaxLineDTO, TaxableItemDTO, } from "@medusajs/framework/types" -import { calculateAmountsWithTax, Modules } from "@medusajs/framework/utils" +import { + calculateAmountsWithTax, + FeatureFlag, + Modules, +} from "@medusajs/framework/utils" import { StoreRequestWithContext } from "../types" +import { applyTranslationsToTaxLines } from "@medusajs/framework/utils" +import TranslationFeatureFlag from "../../../feature-flags/translation" export const wrapVariantsWithTaxPrices = async ( req: StoreRequestWithContext, @@ -31,11 +37,22 @@ export const wrapVariantsWithTaxPrices = async ( const taxService = req.scope.resolve(Modules.TAX) - const taxLines = (await taxService.getTaxLines( + let taxLines = (await taxService.getTaxLines( items, req.taxContext.taxLineContext )) as unknown as ItemTaxLineDTO[] + const isTranslationEnabled = FeatureFlag.isFeatureEnabled( + TranslationFeatureFlag.key + ) + if (isTranslationEnabled) { + taxLines = (await applyTranslationsToTaxLines( + taxLines, + req.locale, + req.scope + )) as ItemTaxLineDTO[] + } + const taxRatesMap = new Map() taxLines.forEach((taxLine) => { diff --git a/packages/medusa/src/api/store/products/helpers.ts b/packages/medusa/src/api/store/products/helpers.ts index 8f991e5c5c..18d156b2bb 100644 --- a/packages/medusa/src/api/store/products/helpers.ts +++ b/packages/medusa/src/api/store/products/helpers.ts @@ -5,8 +5,14 @@ import { MedusaContainer, TaxableItemDTO, } from "@medusajs/framework/types" -import { calculateAmountsWithTax, Modules } from "@medusajs/framework/utils" +import { + applyTranslationsToTaxLines, + calculateAmountsWithTax, + FeatureFlag, + Modules, +} from "@medusajs/framework/utils" import { StoreRequestWithContext } from "../types" +import TranslationFeatureFlag from "../../../feature-flags/translation" export type RequestWithContext< Body, @@ -56,13 +62,24 @@ export const wrapProductsWithTaxPrices = async ( const taxService = req.scope.resolve(Modules.TAX) - const taxRates = (await taxService.getTaxLines( + let taxLines = (await taxService.getTaxLines( products.map(asTaxItem).flat(), req.taxContext.taxLineContext )) as unknown as ItemTaxLineDTO[] + const isTranslationEnabled = FeatureFlag.isFeatureEnabled( + TranslationFeatureFlag.key + ) + if (isTranslationEnabled) { + taxLines = (await applyTranslationsToTaxLines( + taxLines, + req.locale, + req.scope + )) as ItemTaxLineDTO[] + } + const taxRatesMap = new Map() - taxRates.forEach((taxRate) => { + taxLines.forEach((taxRate) => { if (!taxRatesMap.has(taxRate.line_item_id)) { taxRatesMap.set(taxRate.line_item_id, []) } diff --git a/packages/medusa/src/api/utils/middlewares/products/set-tax-context.ts b/packages/medusa/src/api/utils/middlewares/products/set-tax-context.ts index fd5620e5b8..31c37c92d4 100644 --- a/packages/medusa/src/api/utils/middlewares/products/set-tax-context.ts +++ b/packages/medusa/src/api/utils/middlewares/products/set-tax-context.ts @@ -76,6 +76,7 @@ const getTaxLinesContext = async (req: MedusaRequest) => { country_code: req.filterableFields.country_code as string, province_code: req.filterableFields.province as string, }, + locale: req.locale, } as TaxCalculationContext return taxContext