From 11de7e3e3449f3a93724aaeed770a66840ffcaf9 Mon Sep 17 00:00:00 2001 From: Nicolas Gorga <62995075+NicolasGorga@users.noreply.github.com> Date: Fri, 2 Jan 2026 07:26:06 -0300 Subject: [PATCH] feat(translation,core-flows): translate remaining core entities and sync shipping option <> method translations (#14358) * Get translated shipping options step * Apply translations on shipping options list methods. * Pass shipping option naem when refreshing cart shipping methods, so if locale changed, we update the name * Update translatable fields config * Cart shipping method update translation tests * Shipping options translations tests * Add changeset * Update order shipping method translations on update * Remove unnecessary workflow and use step instead * Translate shipping method on order edit * Use new update shipping methods tranlsations step * Draft order shipping method translation sync * Translate shipping method on order exchange * Translate returns shipping methods * Translate claims shipping methods * Remove unnecessary check * Early return * Fix import --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/tall-tires-fix.md | 6 + .../cart/store/cart-translation.spec.ts | 223 ++++++ .../claims/claims-translations.spec.ts | 739 ++++++++++++++++++ .../admin/draft-order-translation.spec.ts | 212 +++++ .../exchanges/exchange-translation.spec.ts | 332 ++++++++ .../order-edit-translation.spec.ts | 144 ++++ .../order/admin/order-translation.spec.ts | 93 ++- .../returns/return-translation.spec.ts | 539 +++++++++++++ .../store/shipping-option-translation.spec.ts | 344 ++++++++ .../core/core-flows/src/cart/utils/fields.ts | 2 + ...-shipping-options-for-cart-with-pricing.ts | 8 +- .../list-shipping-options-for-cart.ts | 8 +- .../refresh-cart-shipping-methods.ts | 1 + .../steps/get-translated-shipping-option.ts | 24 + .../src/draft-order/utils/fields.ts | 1 + .../add-draft-order-shipping-methods.ts | 8 +- .../workflows/update-draft-order.ts | 19 +- .../core/core-flows/src/order/steps/index.ts | 1 + ...ate-order-shipping-methods-translations.ts | 71 ++ .../claim/create-claim-shipping-method.ts | 17 +- .../create-exchange-shipping-method.ts | 10 +- .../create-order-edit-shipping-method.ts | 10 +- .../return/create-return-shipping-method.ts | 10 +- .../src/order/workflows/update-order.ts | 19 +- .../src/utils/translatable-fields.ts | 17 +- 25 files changed, 2831 insertions(+), 27 deletions(-) create mode 100644 .changeset/tall-tires-fix.md create mode 100644 integration-tests/http/__tests__/claims/claims-translations.spec.ts create mode 100644 integration-tests/http/__tests__/returns/return-translation.spec.ts create mode 100644 integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts create mode 100644 packages/core/core-flows/src/common/steps/get-translated-shipping-option.ts create mode 100644 packages/core/core-flows/src/order/steps/update-order-shipping-methods-translations.ts diff --git a/.changeset/tall-tires-fix.md b/.changeset/tall-tires-fix.md new file mode 100644 index 0000000000..53a84e40bb --- /dev/null +++ b/.changeset/tall-tires-fix.md @@ -0,0 +1,6 @@ +--- +"@medusajs/translation": patch +"@medusajs/core-flows": patch +--- + +feat(translation,core-flows): translate remaining core entities and sync shipping option <> method translations 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 11d5c0467b..5c79238e06 100644 --- a/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart-translation.spec.ts @@ -515,6 +515,229 @@ medusaIntegrationTestRunner({ expect(allItemsTranslated).toBe(true) }) }) + + describe("POST /store/carts/:id/shipping-methods (shipping method translation)", () => { + let shippingOption + + beforeEach(async () => { + const stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "translation test location" }, + adminHeaders + ) + ).data.stock_location + + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: `Translation-Test-${shippingProfile.id}`, + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + const fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: `Translation-Test-${shippingProfile.id}`, + 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: "Standard Shipping", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Standard", + description: "Standard shipping option", + code: "standard", + }, + prices: [{ currency_code: "usd", amount: 1000 }], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + + // Create translations for shipping option + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Expédition Standard", + }, + }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Standardversand", + }, + }, + ], + }, + adminHeaders + ) + }) + + it("should add shipping method with translated name when cart has locale", 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 + + await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + const updatedCart = await api + .get( + `/store/carts/${cart.id}?fields=+shipping_methods.name`, + storeHeaders + ) + .then((res) => res.data.cart) + + expect(updatedCart.shipping_methods).toHaveLength(1) + expect(updatedCart.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Expédition Standard", + }) + ) + }) + + it("should update shipping method name when cart locale changes", 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 + + // Add shipping method with French locale + await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + // Verify French translation + let cartResponseAfter = await api.get( + `/store/carts/${cart.id}?fields=+shipping_methods.name`, + storeHeaders + ) + + expect(cartResponseAfter.data.cart.shipping_methods[0].name).toEqual( + "Expédition Standard" + ) + + // Update cart locale to German + await api.post( + `/store/carts/${cart.id}`, + { + locale: "de-DE", + }, + storeHeaders + ) + + // Verify German translation + cartResponseAfter = await api.get( + `/store/carts/${cart.id}?fields=+shipping_methods.name`, + storeHeaders + ) + + expect(cartResponseAfter.data.cart.shipping_methods[0].name).toEqual( + "Standardversand" + ) + }) + + it("should use original shipping option name when no translation exists", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + locale: "ja-JP", + shipping_address: shippingAddressData, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + const updatedCart = await api + .get( + `/store/carts/${cart.id}?fields=+shipping_methods.name`, + storeHeaders + ) + .then((res) => res.data.cart) + + expect(updatedCart.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Standard Shipping", + }) + ) + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/claims/claims-translations.spec.ts b/integration-tests/http/__tests__/claims/claims-translations.spec.ts new file mode 100644 index 0000000000..797dfa59c6 --- /dev/null +++ b/integration-tests/http/__tests__/claims/claims-translations.spec.ts @@ -0,0 +1,739 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { + ClaimReason, + ClaimType, + Modules, + ProductStatus, + RuleOperator, +} from "@medusajs/utils" +import { + adminHeaders, + createAdminUser, +} from "../../../helpers/create-admin-user" + +jest.setTimeout(60000) + +process.env.MEDUSA_FF_TRANSLATION = "true" + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + let order + let customer + let returnShippingOption + let outboundShippingOption + let shippingProfile + let fulfillmentSet + let inventoryItem + let location + let salesChannel + let region + let product + let productExtra + + beforeEach(async () => { + const container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) + + customer = ( + await api.post( + "/admin/customers", + { + first_name: "joe", + email: "joe@admin.com", + }, + adminHeaders + ) + ).data.customer + + // Set up supported locales in the store + const storeModule = container.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" }, + ], + }) + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "Webshop", description: "channel" }, + adminHeaders + ) + ).data.sales_channel + + 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", + }, + adminHeaders + ) + ).data.region + + location = ( + 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: location.id, stocked_quantity: 10 }, + adminHeaders + ) + + await api.post( + `/admin/stock-locations/${location.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + product = ( + await api.post( + "/admin/products", + { + title: "Test product", + status: ProductStatus.PUBLISHED, + options: [{ title: "size", values: ["large", "small"] }], + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + sku: "test-variant", + options: { size: "large" }, + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 1, + }, + ], + prices: [{ currency_code: "usd", amount: 25 }], + }, + ], + }, + adminHeaders + ) + ).data.product + + productExtra = ( + await api.post( + "/admin/products", + { + title: "Extra product", + status: ProductStatus.PUBLISHED, + options: [{ title: "size", values: ["large", "small"] }], + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Extra variant", + sku: "extra-variant", + options: { size: "large" }, + manage_inventory: false, + prices: [{ currency_code: "usd", amount: 50 }], + }, + ], + }, + adminHeaders + ) + ).data.product + + location = ( + await api.post( + `/admin/stock-locations/${location.id}/fulfillment-sets?fields=*fulfillment_sets`, + { name: "Test", type: "test-type" }, + adminHeaders + ) + ).data.stock_location + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${location.fulfillment_sets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${location.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + 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: 1500 }], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + outboundShippingOption = ( + await api.post( + "/admin/shipping-options", + { + name: "Outbound 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: 0 }], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + // Create translations for shipping options + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { name: "Expédition de retour" }, + }, + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { name: "Rückversand" }, + }, + { + reference_id: outboundShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { name: "Expédition sortante" }, + }, + { + reference_id: outboundShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { name: "Ausgehende Versand" }, + }, + ], + }, + adminHeaders + ) + }) + + const createOrderWithLocale = async (locale?: string) => { + const container = getContainer() + const orderModule = container.resolve(Modules.ORDER) + const inventoryModule = container.resolve(Modules.INVENTORY) + + const createdOrder = await orderModule.createOrders({ + region_id: region.id, + email: "foo@bar.com", + locale, + items: [ + { + title: "Custom Item", + variant_id: product.variants[0].id, + quantity: 2, + unit_price: 25, + }, + ], + sales_channel_id: salesChannel.id, + shipping_address: { + customer_id: customer.id, + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + billing_address: { + customer_id: customer.id, + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + shipping_methods: [ + { + name: "Test shipping method", + amount: 10, + data: {}, + }, + ], + currency_code: "usd", + customer_id: customer.id, + transactions: [ + { + amount: 60, + currency_code: "usd", + }, + ], + }) + + await inventoryModule.createReservationItems([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + quantity: 2, + line_item_id: createdOrder.items![0].id, + }, + ]) + + // Fulfill the order + await api.post( + `/admin/orders/${createdOrder.id}/fulfillments`, + { + items: [ + { + id: createdOrder.items![0].id, + quantity: 2, + }, + ], + }, + adminHeaders + ) + + return createdOrder + } + + describe("Claim shipping method translation", () => { + describe("Inbound (return) shipping method", () => { + it("should translate inbound shipping method name using French locale", 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/claims/${claim.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + const inboundShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition de retour", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + + it("should use original inbound shipping method name 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/claims/${claim.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + const inboundShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Return shipping", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + }) + + describe("Outbound shipping method", () => { + it("should translate outbound shipping method", 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/claims/${claim.id}/outbound/shipping-method`, + { shipping_option_id: outboundShippingOption.id }, + adminHeaders + ) + + const outboundShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition sortante", + shipping_option_id: outboundShippingOption.id, + }) + ) + }) + + it("should use original outbound shipping method name 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/claims/${claim.id}/outbound/shipping-method`, + { shipping_option_id: outboundShippingOption.id }, + adminHeaders + ) + + const outboundShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Outbound shipping", + shipping_option_id: outboundShippingOption.id, + }) + ) + }) + }) + + describe("Both inbound and outbound shipping methods", () => { + it("should translate both shipping methods", 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/outbound/items`, + { + items: [ + { + variant_id: productExtra.variants[0].id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const result = await api.post( + `/admin/claims/${claim.id}/outbound/shipping-method`, + { shipping_option_id: outboundShippingOption.id }, + adminHeaders + ) + + const shippingMethods = result.data.order_preview.shipping_methods + + const inboundShippingMethod = shippingMethods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + const outboundShippingMethod = shippingMethods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition de retour", + shipping_option_id: returnShippingOption.id, + }) + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition sortante", + shipping_option_id: outboundShippingOption.id, + }) + ) + }) + + it("should keep translations after confirming claim request", 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}/inbound/items`, + { + items: [ + { + id: order.items![0].id, + quantity: 1, + reason: ClaimReason.PRODUCTION_FAILURE, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/claims/${claim.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + 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 orderResult = await api.get( + `/admin/orders/${order.id}`, + adminHeaders + ) + + const shippingMethods = orderResult.data.order.shipping_methods + + const outboundShippingMethod = shippingMethods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition sortante", + shipping_option_id: outboundShippingOption.id, + }) + ) + }) + }) + }) + }, +}) 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 04f9f3823e..a3718d6f10 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 @@ -208,6 +208,22 @@ medusaIntegrationTestRunner({ locale_code: "de-DE", translations: { title: "Mittel" }, }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Option d'expédition de test", + }, + }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Test-Versandoption", + }, + }, ], }, adminHeaders @@ -480,6 +496,202 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("POST /admin/draft-orders/:id/edit/shipping-methods (add shipping method to draft order)", () => { + it("should translate shipping method added to draft order using draft order 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", + postal_code: "12345", + first_name: "John", + }, + }, + adminHeaders + ) + ).data.draft_order + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit`, + {}, + adminHeaders + ) + + const previewResponse = await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/shipping-methods`, + { shipping_option_id: shippingOption.id }, + adminHeaders + ) + + expect( + previewResponse.data.draft_order_preview.shipping_methods + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Option d'expédition de test", + }), + ]) + ) + + 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).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Option d'expédition de test", + }), + ]) + ) + }) + + it("should have original shipping method name 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", + postal_code: "12345", + first_name: "John", + }, + }, + adminHeaders + ) + ).data.draft_order + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit`, + {}, + adminHeaders + ) + + const previewResponse = await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/shipping-methods`, + { shipping_option_id: shippingOption.id }, + adminHeaders + ) + + expect( + previewResponse.data.draft_order_preview.shipping_methods + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Test shipping option", + }), + ]) + ) + + 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).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Test shipping option", + }), + ]) + ) + }) + }) + + describe("POST /admin/draft-orders/:id (update draft order locale)", () => { + it("should re-translate shipping methods when locale is updated", 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", + 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/shipping-methods`, + { shipping_option_id: shippingOption.id }, + adminHeaders + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}/edit/confirm`, + {}, + adminHeaders + ) + + let updatedDraftOrder = ( + await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders) + ).data.draft_order + + expect(updatedDraftOrder.shipping_methods[0].name).toEqual( + "Option d'expédition de test" + ) + + await api.post( + `/admin/draft-orders/${draftOrder.id}`, + { locale: "de-DE" }, + adminHeaders + ) + + updatedDraftOrder = ( + await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders) + ).data.draft_order + + expect(updatedDraftOrder.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Test-Versandoption", + }) + ) + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts index cfa04ad088..d020afdc2f 100644 --- a/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchange-translation.spec.ts @@ -34,6 +34,7 @@ medusaIntegrationTestRunner({ let stockLocation: { id: string } let shippingOption: { id: string } let outboundShippingOption: { id: string } + let returnShippingOption: { id: string } let inventoryItem: { id: string } beforeAll(async () => { @@ -224,6 +225,33 @@ medusaIntegrationTestRunner({ ) ).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: 500 }], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + await api.post( "/admin/translations/batch", { @@ -270,6 +298,38 @@ medusaIntegrationTestRunner({ locale_code: "de-DE", translations: { title: "Mittel" }, }, + { + reference_id: outboundShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Expédition sortante", + }, + }, + { + reference_id: outboundShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Ausgehende Versand", + }, + }, + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Expédition de retour", + }, + }, + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Rückversand", + }, + }, ], }, adminHeaders @@ -522,6 +582,278 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("Exchange shipping method translation", () => { + it("should translate outbound and inbound shipping methods added during exchange using order locale", async () => { + const order = await createOrderFromCart("fr-FR") + + await api.post( + `/admin/orders/${order.id}/fulfillments`, + { + location_id: stockLocation.id, + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + const exchange = ( + await api.post( + "/admin/exchanges", + { order_id: order.id, description: "Test exchange" }, + adminHeaders + ) + ).data.exchange + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/items`, + { + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + 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 + ) + + const exchangeResult = ( + await api.post( + `/admin/exchanges/${exchange.id}/request`, + {}, + adminHeaders + ) + ).data.exchange + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: outboundShippingOption.id, + name: "Expédition sortante", + }) + ) + + const orderReturn = ( + await api.get( + `/admin/returns/${exchangeResult.return_id}?fields=*shipping_methods`, + adminHeaders + ) + ).data.return + + const inboundShippingMethod = orderReturn.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: returnShippingOption.id, + name: "Expédition de retour", + }) + ) + }) + + it("should translate outbound and inbound shipping methods 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 + ) + + const exchange = ( + await api.post( + "/admin/exchanges", + { order_id: order.id, description: "Test exchange" }, + adminHeaders + ) + ).data.exchange + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/items`, + { + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + 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 + ) + + const exchangeResult = ( + await api.post( + `/admin/exchanges/${exchange.id}/request`, + {}, + adminHeaders + ) + ).data.exchange + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: outboundShippingOption.id, + name: "Ausgehende Versand", + }) + ) + + const orderReturn = ( + await api.get( + `/admin/returns/${exchangeResult.return_id}?fields=*shipping_methods`, + adminHeaders + ) + ).data.return + + const inboundShippingMethod = orderReturn.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: returnShippingOption.id, + name: "Rückversand", + }) + ) + }) + + it("should have original shipping method names when order has no locale", async () => { + const order = await createOrderFromCart() + + await api.post( + `/admin/orders/${order.id}/fulfillments`, + { + location_id: stockLocation.id, + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + const exchange = ( + await api.post( + "/admin/exchanges", + { order_id: order.id, description: "Test exchange" }, + adminHeaders + ) + ).data.exchange + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/items`, + { + items: [{ id: order.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + + await api.post( + `/admin/exchanges/${exchange.id}/inbound/shipping-method`, + { shipping_option_id: returnShippingOption.id }, + adminHeaders + ) + + 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 + ) + + const exchangeResult = ( + await api.post( + `/admin/exchanges/${exchange.id}/request`, + {}, + adminHeaders + ) + ).data.exchange + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const outboundShippingMethod = updatedOrder.shipping_methods.find( + (sm: any) => sm.shipping_option_id === outboundShippingOption.id + ) + + expect(outboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: outboundShippingOption.id, + name: "Outbound shipping", + }) + ) + + const orderReturn = ( + await api.get( + `/admin/returns/${exchangeResult.return_id}?fields=*shipping_methods`, + adminHeaders + ) + ).data.return + + const inboundShippingMethod = orderReturn.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(inboundShippingMethod).toEqual( + expect.objectContaining({ + shipping_option_id: returnShippingOption.id, + name: "Return shipping", + }) + ) + }) + }) }) }, }) 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 35d0d2d284..6a07ba277c 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 @@ -33,6 +33,7 @@ medusaIntegrationTestRunner({ let shippingProfile: { id: string } let stockLocation: { id: string } let shippingOption: { id: string } + let additionalShippingOption: { id: string } let inventoryItem: { id: string } beforeAll(async () => { @@ -196,6 +197,27 @@ medusaIntegrationTestRunner({ ) ).data.shipping_option + additionalShippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: "Additional 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: 500 }], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + await api.post( "/admin/translations/batch", { @@ -242,6 +264,38 @@ medusaIntegrationTestRunner({ locale_code: "de-DE", translations: { title: "Mittel" }, }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Option d'expédition de test", + }, + }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Test-Versandoption", + }, + }, + { + reference_id: additionalShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Option d'expédition supplémentaire", + }, + }, + { + reference_id: additionalShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Zusätzliche Versandoption", + }, + }, ], }, adminHeaders @@ -414,6 +468,96 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("POST /admin/order-edits/:id/shipping-method (add shipping method during order edit)", () => { + it("should translate shipping method added during order edit using order locale", async () => { + const order = await createOrderFromCart("fr-FR") + + await api.post( + "/admin/order-edits", + { order_id: order.id }, + adminHeaders + ) + + const previewResponse = await api.post( + `/admin/order-edits/${order.id}/shipping-method`, + { shipping_option_id: additionalShippingOption.id }, + adminHeaders + ) + + expect(previewResponse.data.order_preview.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: additionalShippingOption.id, + name: "Option d'expédition supplémentaire", + }), + ]) + ) + + await api.post( + `/admin/order-edits/${order.id}/confirm`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + expect(updatedOrder.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: additionalShippingOption.id, + name: "Option d'expédition supplémentaire", + }), + ]) + ) + }) + + it("should have original shipping method name when order has no locale", async () => { + const order = await createOrderFromCart() + + await api.post( + "/admin/order-edits", + { order_id: order.id }, + adminHeaders + ) + + const previewResponse = await api.post( + `/admin/order-edits/${order.id}/shipping-method`, + { shipping_option_id: additionalShippingOption.id }, + adminHeaders + ) + + expect(previewResponse.data.order_preview.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: additionalShippingOption.id, + name: "Additional shipping option", + }), + ]) + ) + + await api.post( + `/admin/order-edits/${order.id}/confirm`, + {}, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + expect(updatedOrder.shipping_methods).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: additionalShippingOption.id, + name: "Additional shipping option", + }), + ]) + ) + }) + }) }) }, }) 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 dc4ce1fd4a..c779a64a74 100644 --- a/integration-tests/http/__tests__/order/admin/order-translation.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order-translation.spec.ts @@ -255,7 +255,7 @@ medusaIntegrationTestRunner({ ) ).data.shipping_option - // Create translations for product and variants + // Create translations for product, variants, and shipping options await api.post( "/admin/translations/batch", { @@ -302,6 +302,22 @@ medusaIntegrationTestRunner({ locale_code: "de-DE", translations: { title: "Mittel" }, }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Option d'expédition de test", + }, + }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Test-Versandoption", + }, + }, ], }, adminHeaders @@ -378,6 +394,30 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should preserve translated shipping methods when order is created from cart with locale", async () => { + const order = await createOrderFromCart("fr-FR") + + expect(order.shipping_methods).toHaveLength(1) + expect(order.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Option d'expédition de test", + }) + ) + }) + + it("should have original shipping method name when order is created without locale", async () => { + const order = await createOrderFromCart() + + expect(order.shipping_methods).toHaveLength(1) + expect(order.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Test shipping option", + }) + ) + }) }) describe("POST /admin/orders/:id (update order locale)", () => { @@ -405,6 +445,32 @@ medusaIntegrationTestRunner({ ) }) + it("should re-translate shipping methods when locale is updated", async () => { + const order = await createOrderFromCart("fr-FR") + + expect(order.shipping_methods[0].name).toEqual( + "Option d'expédition de test" + ) + + await api.post( + `/admin/orders/${order.id}`, + { locale: "de-DE" }, + adminHeaders + ) + + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + expect(updatedOrder.shipping_methods).toHaveLength(1) + expect(updatedOrder.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Test-Versandoption", + }) + ) + }) + it("should not re-translate items when updating other fields", async () => { const order = await createOrderFromCart("fr-FR") @@ -429,6 +495,31 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should not re-translate shipping methods when updating other fields", async () => { + const order = await createOrderFromCart("fr-FR") + + await api.post( + `/admin/orders/${order.id}`, + { email: "updated@example.com" }, + adminHeaders + ) + + const updatedOrder = ( + await api.get( + `/admin/orders/${order.id}?fields=+email,+shipping_methods.name`, + adminHeaders + ) + ).data.order + + expect(updatedOrder.email).toEqual("updated@example.com") + expect(updatedOrder.shipping_methods[0]).toEqual( + expect.objectContaining({ + shipping_option_id: shippingOption.id, + name: "Option d'expédition de test", + }) + ) + }) }) }) }, diff --git a/integration-tests/http/__tests__/returns/return-translation.spec.ts b/integration-tests/http/__tests__/returns/return-translation.spec.ts new file mode 100644 index 0000000000..746c1d2512 --- /dev/null +++ b/integration-tests/http/__tests__/returns/return-translation.spec.ts @@ -0,0 +1,539 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { Modules, RuleOperator } from "@medusajs/utils" +import { + adminHeaders, + createAdminUser, +} from "../../../helpers/create-admin-user" + +jest.setTimeout(60000) + +process.env.MEDUSA_FF_TRANSLATION = "true" + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + let order + let returnShippingOption + let shippingProfile + let fulfillmentSet + let inventoryItem + let location + let salesChannel + let product + + beforeEach(async () => { + const container = getContainer() + await createAdminUser(dbConnection, adminHeaders, container) + + // Set up supported locales in the store + const storeModule = container.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" }, + ], + }) + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "Webshop", description: "channel" }, + adminHeaders + ) + ).data.sales_channel + + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { + name: "Test", + type: "default", + }, + adminHeaders + ) + ).data.shipping_profile + + location = ( + await api.post( + `/admin/stock-locations`, + { + name: "Test location", + }, + adminHeaders + ) + ).data.stock_location + + inventoryItem = ( + await api.post( + `/admin/inventory-items`, + { sku: "inv-1234" }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItem.id}/location-levels`, + { + location_id: location.id, + stocked_quantity: 2, + }, + adminHeaders + ) + + await api.post( + `/admin/stock-locations/${location.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + product = ( + await api.post( + "/admin/products", + { + title: "Test product", + options: [{ title: "size", values: ["x", "l"] }], + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + sku: "test-variant", + options: { size: "l" }, + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 1, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 10, + }, + ], + }, + ], + }, + adminHeaders + ) + ).data.product + + location = ( + await api.post( + `/admin/stock-locations/${location.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: "Test", + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${location.fulfillment_sets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${location.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + 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 + + // Create translations for the return shipping option + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Expédition de retour", + }, + }, + { + reference_id: returnShippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Rückversand", + }, + }, + ], + }, + adminHeaders + ) + }) + + const createOrderWithLocale = async (locale?: string) => { + const container = getContainer() + const orderModule = container.resolve(Modules.ORDER) + const inventoryModule = container.resolve(Modules.INVENTORY) + + const createdOrder = await orderModule.createOrders({ + region_id: "test_region_id", + email: "foo@bar.com", + locale, + items: [ + { + title: "Custom Item", + variant_id: product.variants[0].id, + quantity: 2, + unit_price: 25, + }, + ], + sales_channel_id: salesChannel.id, + shipping_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + phone: "12345", + }, + billing_address: { + first_name: "Test", + last_name: "Test", + address_1: "Test", + city: "Test", + country_code: "US", + postal_code: "12345", + }, + shipping_methods: [ + { + name: "Test shipping method", + amount: 10, + data: {}, + }, + ], + currency_code: "usd", + customer_id: "joe", + }) + + await inventoryModule.createReservationItems([ + { + inventory_item_id: inventoryItem.id, + location_id: location.id, + quantity: 2, + line_item_id: createdOrder.items![0].id, + }, + ]) + + // Fulfill the order + await api.post( + `/admin/orders/${createdOrder.id}/fulfillments`, + { + items: [ + { + id: createdOrder.items![0].id, + quantity: 2, + }, + ], + }, + adminHeaders + ) + + return createdOrder + } + + describe("Return shipping method translation", () => { + it("should translate return shipping method name using French locale", async () => { + order = await createOrderWithLocale("fr-FR") + + const returnResult = await api.post( + "/admin/returns", + { + order_id: order.id, + location_id: location.id, + }, + adminHeaders + ) + + const returnId = returnResult.data.return.id + const item = order.items[0] + + await api.post( + `/admin/returns/${returnId}/request-items`, + { + items: [ + { + id: item.id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/returns/${returnId}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + const returnShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition de retour", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + + it("should use original shipping method name when order has no locale", async () => { + order = await createOrderWithLocale() + + const returnResult = await api.post( + "/admin/returns", + { + order_id: order.id, + location_id: location.id, + }, + adminHeaders + ) + + const returnId = returnResult.data.return.id + const item = order.items[0] + + await api.post( + `/admin/returns/${returnId}/request-items`, + { + items: [ + { + id: item.id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/returns/${returnId}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + const returnShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod).toEqual( + expect.objectContaining({ + name: "Return shipping", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + + it("should use original name when locale has no translation", async () => { + order = await createOrderWithLocale("en-US") + + const returnResult = await api.post( + "/admin/returns", + { + order_id: order.id, + location_id: location.id, + }, + adminHeaders + ) + + const returnId = returnResult.data.return.id + const item = order.items[0] + + await api.post( + `/admin/returns/${returnId}/request-items`, + { + items: [ + { + id: item.id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/returns/${returnId}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + const returnShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod).toEqual( + expect.objectContaining({ + name: "Return shipping", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + + it("should translate return shipping method with custom price", async () => { + order = await createOrderWithLocale("fr-FR") + + const returnResult = await api.post( + "/admin/returns", + { + order_id: order.id, + location_id: location.id, + }, + adminHeaders + ) + + const returnId = returnResult.data.return.id + const item = order.items[0] + + await api.post( + `/admin/returns/${returnId}/request-items`, + { + items: [ + { + id: item.id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + const shippingMethodResult = await api.post( + `/admin/returns/${returnId}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + custom_amount: 500, + }, + adminHeaders + ) + + const returnShippingMethod = + shippingMethodResult.data.order_preview.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition de retour", + shipping_option_id: returnShippingOption.id, + amount: 500, + }) + ) + }) + + it("should keep translation after confirming return request", async () => { + order = await createOrderWithLocale("fr-FR") + + const returnResult = await api.post( + "/admin/returns", + { + order_id: order.id, + location_id: location.id, + }, + adminHeaders + ) + + const returnId = returnResult.data.return.id + const item = order.items[0] + + await api.post( + `/admin/returns/${returnId}/request-items`, + { + items: [ + { + id: item.id, + quantity: 1, + }, + ], + }, + adminHeaders + ) + + await api.post( + `/admin/returns/${returnId}/shipping-method`, + { + shipping_option_id: returnShippingOption.id, + }, + adminHeaders + ) + + await api.post(`/admin/returns/${returnId}/request`, {}, adminHeaders) + + const orderResult = await api.get( + `/admin/orders/${order.id}`, + adminHeaders + ) + + const returnShippingMethod = + orderResult.data.order.shipping_methods.find( + (sm: any) => sm.shipping_option_id === returnShippingOption.id + ) + + expect(returnShippingMethod).toEqual( + expect.objectContaining({ + name: "Expédition de retour", + shipping_option_id: returnShippingOption.id, + }) + ) + }) + }) + }, +}) diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts new file mode 100644 index 0000000000..cebc8f14d2 --- /dev/null +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option-translation.spec.ts @@ -0,0 +1,344 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { + createAdminUser, + generatePublishableKey, + generateStoreHeaders, +} from "../../../../helpers/create-admin-user" +import { Modules, ProductStatus } from "@medusajs/utils" + +jest.setTimeout(50000) + +process.env.MEDUSA_FF_TRANSLATION = "true" + +const env = {} +const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } } + +medusaIntegrationTestRunner({ + env, + testSuite: ({ dbConnection, getContainer, api }) => { + describe("Store: Shipping Option API (with translations)", () => { + let appContainer + let salesChannel + let region + let product + let stockLocation + let shippingProfile + let fulfillmentSet + let shippingOption + let storeHeaders + + beforeAll(async () => { + appContainer = getContainer() + }) + + afterAll(async () => { + delete process.env.MEDUSA_FF_TRANSLATION + }) + + beforeEach(async () => { + const publishableKey = await generatePublishableKey(appContainer) + storeHeaders = generateStoreHeaders({ publishableKey }) + + await createAdminUser(dbConnection, adminHeaders, appContainer) + + // Set up store locales + 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: "Test", type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "first channel", description: "channel" }, + adminHeaders + ) + ).data.sales_channel + + product = ( + await api.post( + "/admin/products", + { + title: "Test fixture", + status: ProductStatus.PUBLISHED, + options: [ + { title: "size", values: ["large", "small"] }, + { title: "color", values: ["green"] }, + ], + shipping_profile_id: shippingProfile.id, + variants: [ + { + title: "Test variant", + manage_inventory: false, + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + options: { + size: "large", + color: "green", + }, + }, + ], + }, + adminHeaders + ) + ).data.product + + stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location + + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + 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 + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [ + { type: "country", country_code: "us" }, + { type: "country", country_code: "dk" }, + ], + }, + 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, + }, + { + region_id: region.id, + amount: 1100, + }, + ], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + + // Create translations for shipping option + await api.post( + "/admin/translations/batch", + { + create: [ + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "fr-FR", + translations: { + name: "Option d'expédition test", + }, + }, + { + reference_id: shippingOption.id, + reference: "shipping_option", + locale_code: "de-DE", + translations: { + name: "Test-Versandoption", + }, + }, + ], + }, + adminHeaders + ) + }) + + describe("GET /store/shipping-options?cart_id=", () => { + it("should return translated shipping options when cart has locale", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + region_id: region.id, + sales_channel_id: salesChannel.id, + currency_code: "usd", + locale: "fr-FR", + email: "test@admin.com", + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + const shippingOptionsResponse = await api.get( + `/store/shipping-options?cart_id=${cart.id}`, + storeHeaders + ) + + expect(shippingOptionsResponse.status).toEqual(200) + expect(shippingOptionsResponse.data.shipping_options).toHaveLength(1) + expect(shippingOptionsResponse.data.shipping_options[0]).toEqual( + expect.objectContaining({ + id: shippingOption.id, + name: "Option d'expédition test", + }) + ) + }) + + it("should return translated shipping options when locale is changed", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + region_id: region.id, + sales_channel_id: salesChannel.id, + currency_code: "usd", + locale: "fr-FR", + email: "test@admin.com", + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + // Verify French translation + let shippingOptionsResponse = await api.get( + `/store/shipping-options?cart_id=${cart.id}`, + storeHeaders + ) + + expect(shippingOptionsResponse.data.shipping_options[0].name).toEqual( + "Option d'expédition test" + ) + + // Update cart locale to German + await api.post( + `/store/carts/${cart.id}`, + { + locale: "de-DE", + }, + storeHeaders + ) + + // Verify German translation + shippingOptionsResponse = await api.get( + `/store/shipping-options?cart_id=${cart.id}`, + storeHeaders + ) + + expect(shippingOptionsResponse.data.shipping_options[0].name).toEqual( + "Test-Versandoption" + ) + }) + + it("should return original shipping option name when no translation exists", async () => { + const cartResponse = await api.post( + `/store/carts`, + { + region_id: region.id, + sales_channel_id: salesChannel.id, + currency_code: "usd", + locale: "ja-JP", + email: "test@admin.com", + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + storeHeaders + ) + + const cart = cartResponse.data.cart + + const shippingOptionsResponse = await api.get( + `/store/shipping-options?cart_id=${cart.id}`, + storeHeaders + ) + + expect(shippingOptionsResponse.status).toEqual(200) + expect(shippingOptionsResponse.data.shipping_options[0]).toEqual( + expect.objectContaining({ + id: shippingOption.id, + name: "Test shipping option", + }) + ) + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/cart/utils/fields.ts b/packages/core/core-flows/src/cart/utils/fields.ts index 70b497d3bb..1481a59fa6 100644 --- a/packages/core/core-flows/src/cart/utils/fields.ts +++ b/packages/core/core-flows/src/cart/utils/fields.ts @@ -142,6 +142,7 @@ export const cartFieldsForPricingContext = [ "shipping_address.postal_code", "item_total", "total", + "locale", "customer.id", "email", "customer.groups.id", @@ -188,6 +189,7 @@ export const productVariantsFields = [ // ensure that at least these fields are present when fetching cart for caluclating shipping options prices export const cartFieldsForCalculateShippingOptionsPrices = [ "id", + "locale", "items.*", "items.variant.id", "items.variant.product.id", diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts index 4ddcdf1be0..b6dabd749d 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts @@ -23,6 +23,7 @@ import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { calculateShippingOptionsPricesStep } from "../../fulfillment" import { cartFieldsForCalculateShippingOptionsPrices } from "../utils/fields" import { shippingOptionsContextResult } from "../utils/schemas" +import { getTranslatedShippingOptionsStep } from "../../common/steps/get-translated-shipping-option" const COMMON_OPTIONS_FIELDS = [ "id", @@ -397,7 +398,12 @@ export const listShippingOptionsForCartWithPricingWorkflow = createWorkflow( } ) - return new WorkflowResponse(shippingOptionsWithPrice, { + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptionsWithPrice, + locale: cart.locale, + }) + + return new WorkflowResponse(translatedShippingOptions as any[], { hooks: [setShippingOptionsContext] as const, }) } diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts index d752128f07..1de65c17ef 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts @@ -24,6 +24,7 @@ import { pricingContextResult, shippingOptionsContextResult, } from "../utils/schemas" +import { getTranslatedShippingOptionsStep } from "../../common/steps/get-translated-shipping-option" export const listShippingOptionsForCartWorkflowId = "list-shipping-options-for-cart" @@ -359,7 +360,12 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( }) ) - return new WorkflowResponse(shippingOptionsWithPrice, { + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptionsWithPrice, + locale: cart.locale, + }) + + return new WorkflowResponse(translatedShippingOptions as any[], { hooks: [setPricingContext, setShippingOptionsContext] as const, }) } diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts index 09114b509f..d5af95dcd4 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts @@ -192,6 +192,7 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow( return { id: shippingMethod.id, shipping_option_id: shippingOption.id, + name: shippingOption.name, amount: shippingOption.calculated_price.calculated_amount, is_tax_inclusive: shippingOption.calculated_price diff --git a/packages/core/core-flows/src/common/steps/get-translated-shipping-option.ts b/packages/core/core-flows/src/common/steps/get-translated-shipping-option.ts new file mode 100644 index 0000000000..1af0cc61b3 --- /dev/null +++ b/packages/core/core-flows/src/common/steps/get-translated-shipping-option.ts @@ -0,0 +1,24 @@ +import { applyTranslations } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { ShippingOptionDTO } from "@medusajs/types" + +export const getTranslatedShippingOptionsStepId = + "get-translated-shipping-options" + +export interface GetTranslatedShippingOptionsStepInput { + shippingOptions: ShippingOptionDTO[] + locale: string +} + +export const getTranslatedShippingOptionsStep = createStep( + getTranslatedShippingOptionsStepId, + async (data: GetTranslatedShippingOptionsStepInput, { container }) => { + await applyTranslations({ + localeCode: data.locale, + objects: data.shippingOptions, + container, + }) + + return new StepResponse(data.shippingOptions) + } +) diff --git a/packages/core/core-flows/src/draft-order/utils/fields.ts b/packages/core/core-flows/src/draft-order/utils/fields.ts index 76311f6475..3075b4ef89 100644 --- a/packages/core/core-flows/src/draft-order/utils/fields.ts +++ b/packages/core/core-flows/src/draft-order/utils/fields.ts @@ -6,6 +6,7 @@ export const draftOrderFieldsForRefreshSteps = [ "metadata", "sales_channel_id", "region_id", + "locale", "region.*", "items.*", "items.product.id", diff --git a/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts index 68b77a1fa3..118d8b84de 100644 --- a/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts +++ b/packages/core/core-flows/src/draft-order/workflows/add-draft-order-shipping-methods.ts @@ -31,6 +31,7 @@ import { validateDraftOrderChangeStep } from "../steps/validate-draft-order-chan import { draftOrderFieldsForRefreshSteps } from "../utils/fields" import { acquireLockStep, releaseLockStep } from "../../locking" import { computeDraftOrderAdjustmentsWorkflow } from "./compute-draft-order-adjustments" +import { getTranslatedShippingOptionsStep } from "../../common/steps/get-translated-shipping-option" const validateShippingOptionStep = createStep( "validate-shipping-option", @@ -148,12 +149,17 @@ export const addDraftOrderShippingMethodsWorkflow = createWorkflow( }, }).config({ name: "fetch-shipping-option" }) + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptions, + locale: order.locale!, + }) + validateShippingOptionStep({ shippingOptions, input }) const shippingMethodInput = transform( { relatedEntity: { order_id: order.id }, - shippingOptions, + shippingOptions: translatedShippingOptions, customPrice: input.custom_amount as any, // Need to cast this to any otherwise the type becomes to complex. orderChange, input, 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 2fde9de495..f1516e696c 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 @@ -9,6 +9,7 @@ import { Modules, OrderWorkflowEvents } from "@medusajs/framework/utils" import { createStep, createWorkflow, + parallelize, StepResponse, transform, when, @@ -21,6 +22,7 @@ import { previewOrderChangeStep, registerOrderChangesStep, updateOrderItemsTranslationsStep, + updateOrderShippingMethodsTranslationsStep, } from "../../order" import { validateDraftOrderStep } from "../steps/validate-draft-order" @@ -179,6 +181,9 @@ export const updateDraftOrderWorkflow = createWorkflow( "locale", "shipping_address.*", "billing_address.*", + "shipping_methods.id", + "shipping_methods.name", + "shipping_methods.shipping_option_id", "metadata", ], variables: { @@ -340,10 +345,16 @@ export const updateDraftOrderWorkflow = createWorkflow( when({ input, order }, ({ input, order }) => { return !!input.locale && input.locale !== order.locale }).then(() => { - updateOrderItemsTranslationsStep({ - order_id: input.id, - locale: input.locale!, - }) + parallelize( + updateOrderShippingMethodsTranslationsStep({ + locale: input.locale!, + shippingMethods: order.shipping_methods, + }), + updateOrderItemsTranslationsStep({ + order_id: input.id, + locale: input.locale!, + }) + ) }) emitEventStep({ diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index 3cea34b177..9d4e4d701f 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -37,5 +37,6 @@ export * from "./set-tax-lines-for-items" export * from "./update-order-change-actions" export * from "./update-order-changes" export * from "./update-order-items-translations" +export * from "./update-order-shipping-methods-translations" export * from "./update-orders" export * from "./update-shipping-methods" diff --git a/packages/core/core-flows/src/order/steps/update-order-shipping-methods-translations.ts b/packages/core/core-flows/src/order/steps/update-order-shipping-methods-translations.ts new file mode 100644 index 0000000000..dfae5759a5 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/update-order-shipping-methods-translations.ts @@ -0,0 +1,71 @@ +import { + applyTranslations, + ContainerRegistrationKeys, + FeatureFlag, + Modules, +} from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { OrderShippingMethodDTO } from "@medusajs/types" + +export const updateOrderShippingMethodsTranslationsStepId = + "update-order-shipping-methods-translations" + +export interface UpdateOrderShippingMethodsTranslationsStepInput { + shippingMethods: OrderShippingMethodDTO[] + locale: string +} + +export const updateOrderShippingMethodsTranslationsStep = createStep( + updateOrderShippingMethodsTranslationsStepId, + async ( + data: UpdateOrderShippingMethodsTranslationsStepInput, + { container } + ) => { + const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation") + + if (!isTranslationEnabled || !data.locale || !data.shippingMethods.length) { + return new StepResponse(data.shippingMethods) + } + + const query = container.resolve(ContainerRegistrationKeys.QUERY) + const orderModuleService = container.resolve(Modules.ORDER) + + const { data: translatedShippingOptions } = await query.graph({ + entity: "shipping_option", + fields: ["id", "name"], + filters: { + id: data.shippingMethods.map((sm) => sm.shipping_option_id), + }, + }) + + await applyTranslations({ + localeCode: data.locale, + objects: translatedShippingOptions, + container, + }) + + const shippingOptionTranslationMap = new Map( + translatedShippingOptions.map((tos) => [tos.id, tos.name]) + ) + const updatedShippingMethods = + await orderModuleService.updateOrderShippingMethods( + data.shippingMethods.map((sm) => ({ + ...sm, + name: sm.shipping_option_id + ? shippingOptionTranslationMap.get(sm.shipping_option_id) + : sm.name, + })) + ) + + return new StepResponse(updatedShippingMethods, data.shippingMethods) + }, + async (dataBeforeUpdate, { container }) => { + if (!dataBeforeUpdate?.length) { + return + } + + const orderModuleService = container.resolve(Modules.ORDER) + + await orderModuleService.updateOrderShippingMethods(dataBeforeUpdate) + } +) diff --git a/packages/core/core-flows/src/order/workflows/claim/create-claim-shipping-method.ts b/packages/core/core-flows/src/order/workflows/claim/create-claim-shipping-method.ts index b121bc6d16..9ee163e62e 100644 --- a/packages/core/core-flows/src/order/workflows/claim/create-claim-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/claim/create-claim-shipping-method.ts @@ -24,6 +24,7 @@ import { prepareShippingMethod } from "../../utils/prepare-shipping-method" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { updateOrderTaxLinesWorkflow } from "../update-tax-lines" import { fetchShippingOptionForOrderWorkflow } from "../fetch-shipping-option" +import { getTranslatedShippingOptionsStep } from "../../../common/steps/get-translated-shipping-option" /** * The data to validate that a shipping method can be created for a claim. @@ -162,7 +163,14 @@ export const createClaimShippingMethodWorkflow = createWorkflow( const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", - fields: ["id", "status", "region_id", "currency_code", "canceled_at"], + fields: [ + "id", + "status", + "region_id", + "currency_code", + "canceled_at", + "locale", + ], variables: { id: orderClaim.order_id }, list: false, throw_if_key_not_found: true, @@ -227,12 +235,17 @@ export const createClaimShippingMethodWorkflow = createWorkflow( return [shippingOption] }) + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptions, + locale: order.locale!, + }) + createClaimShippingMethodValidationStep({ order, orderClaim, orderChange }) const shippingMethodInput = transform( { relatedEntity: orderClaim, - shippingOptions, + shippingOptions: translatedShippingOptions, customPrice: input.custom_amount, orderChange, input, diff --git a/packages/core/core-flows/src/order/workflows/exchange/create-exchange-shipping-method.ts b/packages/core/core-flows/src/order/workflows/exchange/create-exchange-shipping-method.ts index 718965521c..db97a08ea4 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/create-exchange-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/create-exchange-shipping-method.ts @@ -24,6 +24,7 @@ import { prepareShippingMethod } from "../../utils/prepare-shipping-method" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { updateOrderTaxLinesWorkflow } from "../update-tax-lines" import { fetchShippingOptionForOrderWorkflow } from "../fetch-shipping-option" +import { getTranslatedShippingOptionsStep } from "../../../common/steps/get-translated-shipping-option" /** * The data to validate that a shipping method can be created for an exchange. @@ -163,7 +164,7 @@ export const createExchangeShippingMethodWorkflow = createWorkflow( const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", - fields: ["id", "status", "currency_code", "canceled_at"], + fields: ["id", "status", "currency_code", "canceled_at", "locale"], variables: { id: orderExchange.order_id }, list: false, throw_if_key_not_found: true, @@ -228,6 +229,11 @@ export const createExchangeShippingMethodWorkflow = createWorkflow( return [shippingOption] }) + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptions, + locale: order.locale!, + }) + createExchangeShippingMethodValidationStep({ order, orderExchange, @@ -237,7 +243,7 @@ export const createExchangeShippingMethodWorkflow = createWorkflow( const shippingMethodInput = transform( { relatedEntity: orderExchange, - shippingOptions, + shippingOptions: translatedShippingOptions, customPrice: input.custom_amount, orderChange, input, diff --git a/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts b/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts index e154327523..57a379896e 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/create-order-edit-shipping-method.ts @@ -25,6 +25,7 @@ import { import { prepareShippingMethod } from "../../utils/prepare-shipping-method" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { updateOrderTaxLinesWorkflow } from "../update-tax-lines" +import { getTranslatedShippingOptionsStep } from "../../../common/steps/get-translated-shipping-option" /** * The data to validate that a shipping method can be created for an order edit. @@ -163,7 +164,7 @@ export const createOrderEditShippingMethodWorkflow = createWorkflow( const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", - fields: ["id", "status", "currency_code", "canceled_at"], + fields: ["id", "status", "currency_code", "canceled_at", "locale"], variables: { id: input.order_id }, list: false, throw_if_key_not_found: true, @@ -220,10 +221,15 @@ export const createOrderEditShippingMethodWorkflow = createWorkflow( list: false, }).config({ name: "order-change-query" }) + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptions, + locale: order.locale!, + }) + const shippingMethodInput = transform( { relatedEntity: { order_id: order.id }, - shippingOptions, + shippingOptions: translatedShippingOptions, customPrice: input.custom_amount, orderChange, input, diff --git a/packages/core/core-flows/src/order/workflows/return/create-return-shipping-method.ts b/packages/core/core-flows/src/order/workflows/return/create-return-shipping-method.ts index 77cc3bcc2b..2d5516742b 100644 --- a/packages/core/core-flows/src/order/workflows/return/create-return-shipping-method.ts +++ b/packages/core/core-flows/src/order/workflows/return/create-return-shipping-method.ts @@ -23,6 +23,7 @@ import { prepareShippingMethod } from "../../utils/prepare-shipping-method" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { updateOrderTaxLinesWorkflow } from "../update-tax-lines" import { fetchShippingOptionForOrderWorkflow } from "../fetch-shipping-option" +import { getTranslatedShippingOptionsStep } from "../../../common/steps/get-translated-shipping-option" /** * The data to validate that a shipping method can be created for a return. @@ -153,7 +154,7 @@ export const createReturnShippingMethodWorkflow = createWorkflow( const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", - fields: ["id", "status", "currency_code", "canceled_at"], + fields: ["id", "status", "currency_code", "canceled_at", "locale"], variables: { id: orderReturn.order_id }, list: false, throw_if_key_not_found: true, @@ -202,6 +203,11 @@ export const createReturnShippingMethodWorkflow = createWorkflow( return [shippingOption] }) + const translatedShippingOptions = getTranslatedShippingOptionsStep({ + shippingOptions: shippingOptions, + locale: order.locale!, + }) + createReturnShippingMethodValidationStep({ order, orderReturn, @@ -211,7 +217,7 @@ export const createReturnShippingMethodWorkflow = createWorkflow( const shippingMethodInput = transform( { relatedEntity: orderReturn, - shippingOptions, + shippingOptions: translatedShippingOptions, customPrice: input.custom_amount, orderChange, input, 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 79872cd62b..c17f29e250 100644 --- a/packages/core/core-flows/src/order/workflows/update-order.ts +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -14,6 +14,7 @@ import { WorkflowResponse, createStep, createWorkflow, + parallelize, transform, when, } from "@medusajs/framework/workflows-sdk" @@ -23,6 +24,7 @@ import { previewOrderChangeStep, registerOrderChangesStep, updateOrderItemsTranslationsStep, + updateOrderShippingMethodsTranslationsStep, updateOrdersStep, } from "../steps" import { throwIfOrderIsCancelled } from "../utils/order-validation" @@ -134,6 +136,9 @@ export const updateOrderWorkflow = createWorkflow( "shipping_address.*", "billing_address.*", "metadata", + "shipping_methods.id", + "shipping_methods.name", + "shipping_methods.shipping_option_id", ], filters: { id: input.id }, options: { throwIfKeyNotFound: true }, @@ -261,10 +266,16 @@ export const updateOrderWorkflow = createWorkflow( when("locale-changed", { input, order }, ({ input, order }) => { return !!input.locale && input.locale !== order.locale }).then(() => { - updateOrderItemsTranslationsStep({ - order_id: input.id, - locale: input.locale!, - }) + parallelize( + updateOrderItemsTranslationsStep({ + order_id: input.id, + locale: input.locale!, + }), + updateOrderShippingMethodsTranslationsStep({ + locale: input.locale!, + shippingMethods: order.shipping_methods, + }) + ) }) emitEventStep({ diff --git a/packages/modules/translation/src/utils/translatable-fields.ts b/packages/modules/translation/src/utils/translatable-fields.ts index 6baadd5879..45483191ff 100644 --- a/packages/modules/translation/src/utils/translatable-fields.ts +++ b/packages/modules/translation/src/utils/translatable-fields.ts @@ -11,9 +11,11 @@ export const PRODUCT_CATEGORY_TRANSLATABLE_FIELDS = ["name", "description"] export const PRODUCT_TAG_TRANSLATABLE_FIELDS = ["value"] export const PRODUCT_OPTION_TRANSLATABLE_FIELDS = ["title"] export const PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS = ["value"] - -// export const SHIPPING_OPTION_TRANSLATABLE_FIELDS = ["name"] -// export const SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS = ["label", "description"] +export const REGION_TRANSLATABLE_FIELDS = ["name"] +export const CUSTOMER_GROUP_TRANSLATABLE_FIELDS = ["name"] +export const SHIPPING_OPTION_TRANSLATABLE_FIELDS = ["name"] +export const SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS = ["label", "description"] +export const TAX_RATE_TRANSLATABLE_FIELDS = ["name"] // export const RETURN_REASON_TRANSLATABLE_FIELDS = [ // "value", @@ -30,9 +32,10 @@ export const translatableFieldsConfig = { product_tag: PRODUCT_TAG_TRANSLATABLE_FIELDS, product_option: PRODUCT_OPTION_TRANSLATABLE_FIELDS, product_option_value: PRODUCT_OPTION_VALUE_TRANSLATABLE_FIELDS, - - // shipping_option: SHIPPING_OPTION_TRANSLATABLE_FIELDS, - // shipping_option_type: SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS, - + region: REGION_TRANSLATABLE_FIELDS, + customer_group: CUSTOMER_GROUP_TRANSLATABLE_FIELDS, + shipping_option: SHIPPING_OPTION_TRANSLATABLE_FIELDS, + shipping_option_type: SHIPPING_OPTION_TYPE_TRANSLATABLE_FIELDS, + tax_rate: TAX_RATE_TRANSLATABLE_FIELDS, // return_reason: RETURN_REASON_TRANSLATABLE_FIELDS, }