feat(): Sync order translations (#14267)
* feat(): Sync order translations * feat(): Sync order translations * tests * Create tender-melons-develop.md * fix tests * cleanup * cleanup
This commit is contained in:
1 parent
fe314ab5bc
commit
f13c23a4b7
30 files changed
+2271
-74
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/order": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(): Sync order translations
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
import { Modules, ProductStatus } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||||
|
|
||||||
|
jest.setTimeout(300000)
|
||||||
|
|
||||||
|
process.env.MEDUSA_FF_TRANSLATION = "true"
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
describe("Admin Draft Order Translation API", () => {
|
||||||
|
let appContainer: MedusaContainer
|
||||||
|
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 }
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupTaxStructure(appContainer.resolve(Modules.TAX))
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
|
|
||||||
|
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", is_default: true },
|
||||||
|
{ 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
|
||||||
|
|
||||||
|
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" },
|
||||||
|
manage_inventory: false,
|
||||||
|
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
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/translations/batch",
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: {
|
||||||
|
title: "T-Shirt Medusa",
|
||||||
|
description: "Un t-shirt en coton confortable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: {
|
||||||
|
title: "Medusa T-Shirt DE",
|
||||||
|
description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Petit" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Klein" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Moyen" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Mittel" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/draft-orders/:id/edit/items (add items to draft order)", () => {
|
||||||
|
it("should translate items 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",
|
||||||
|
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/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedDraftOrder = (
|
||||||
|
await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders)
|
||||||
|
).data.draft_order
|
||||||
|
|
||||||
|
expect(updatedDraftOrder.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "T-Shirt Medusa",
|
||||||
|
product_description: "Un t-shirt en coton confortable",
|
||||||
|
variant_title: "Petit",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have original values 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
|
||||||
|
)
|
||||||
|
|
||||||
|
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/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedDraftOrder = (
|
||||||
|
await api.get(`/admin/draft-orders/${draftOrder.id}`, adminHeaders)
|
||||||
|
).data.draft_order
|
||||||
|
|
||||||
|
expect(updatedDraftOrder.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt",
|
||||||
|
product_description: "A comfortable cotton t-shirt",
|
||||||
|
variant_title: "Small",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should translate multiple items added to draft order", async () => {
|
||||||
|
const draftOrder = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/draft-orders",
|
||||||
|
{
|
||||||
|
email: "test@test.com",
|
||||||
|
region_id: region.id,
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
locale: "de-DE",
|
||||||
|
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/items`,
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ variant_id: product.variants[0].id, quantity: 1 },
|
||||||
|
{ variant_id: product.variants[1].id, quantity: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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.items).toHaveLength(2)
|
||||||
|
|
||||||
|
const smallItem = updatedDraftOrder.items.find(
|
||||||
|
(item) => item.variant_id === product.variants[0].id
|
||||||
|
)
|
||||||
|
const mediumItem = updatedDraftOrder.items.find(
|
||||||
|
(item) => item.variant_id === product.variants[1].id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(smallItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt DE",
|
||||||
|
variant_title: "Klein",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(mediumItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt DE",
|
||||||
|
variant_title: "Mittel",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/draft-orders/:id (update draft order locale)", () => {
|
||||||
|
it("should re-translate all items 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/items`,
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{ variant_id: product.variants[0].id, quantity: 1 },
|
||||||
|
{ variant_id: product.variants[1].id, quantity: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
||||||
|
const frenchSmallItem = updatedDraftOrder.items.find(
|
||||||
|
(item) => item.variant_id === product.variants[0].id
|
||||||
|
)
|
||||||
|
expect(frenchSmallItem.variant_title).toEqual("Petit")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const germanSmallItem = updatedDraftOrder.items.find(
|
||||||
|
(item) => item.variant_id === product.variants[0].id
|
||||||
|
)
|
||||||
|
const germanMediumItem = updatedDraftOrder.items.find(
|
||||||
|
(item) => item.variant_id === product.variants[1].id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(germanSmallItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt DE",
|
||||||
|
product_description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
variant_title: "Klein",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(germanMediumItem).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt DE",
|
||||||
|
variant_title: "Mittel",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
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("Exchange 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 outboundShippingOption: { id: string }
|
||||||
|
let inventoryItem: { id: string }
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
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", is_default: true },
|
||||||
|
{ 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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/translations/batch",
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: {
|
||||||
|
title: "T-Shirt Medusa",
|
||||||
|
description: "Un t-shirt en coton confortable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: {
|
||||||
|
title: "Medusa T-Shirt DE",
|
||||||
|
description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Petit" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Klein" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Moyen" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Mittel" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
||||||
|
return (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Exchange items translation", () => {
|
||||||
|
it("should translate new items in 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
|
||||||
|
|
||||||
|
// Add inbound item (item being returned)
|
||||||
|
await api.post(
|
||||||
|
`/admin/exchanges/${exchange.id}/inbound/items`,
|
||||||
|
{
|
||||||
|
items: [{ id: order.items[0].id, quantity: 1 }],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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: "T-Shirt Medusa",
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have original values 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
product_description: "A comfortable cotton t-shirt",
|
||||||
|
variant_title: "Medium",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,419 @@
|
|||||||
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
import { Modules, ProductStatus } 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("Order Edit 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 inventoryItem: { id: string }
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
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", is_default: true },
|
||||||
|
{ 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
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
"/admin/translations/batch",
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: {
|
||||||
|
title: "T-Shirt Medusa",
|
||||||
|
description: "Un t-shirt en coton confortable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: {
|
||||||
|
title: "Medusa T-Shirt DE",
|
||||||
|
description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Petit" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Klein" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Moyen" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Mittel" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
||||||
|
return (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("POST /admin/order-edits/:id/items (add items during order edit)", () => {
|
||||||
|
it("should translate new items 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
|
||||||
|
)
|
||||||
|
|
||||||
|
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: "T-Shirt Medusa",
|
||||||
|
variant_title: "Moyen",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have original values 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}/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",
|
||||||
|
product_description: "A comfortable cotton t-shirt",
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
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("Admin Order 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 outboundShippingOption: { id: string }
|
||||||
|
let inventoryItem: { id: string }
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
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", is_default: true },
|
||||||
|
{ 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
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create product with description for translation
|
||||||
|
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
|
||||||
|
|
||||||
|
// Maintain predictable variants order
|
||||||
|
const variantSmall = product.variants.find((v) => v.title === "Small")
|
||||||
|
const variantMedium = product.variants.find((v) => v.title === "Medium")
|
||||||
|
product.variants = [variantSmall!, variantMedium!]
|
||||||
|
|
||||||
|
// Setup fulfillment
|
||||||
|
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: 500 }],
|
||||||
|
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 product and variants
|
||||||
|
await api.post(
|
||||||
|
"/admin/translations/batch",
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: {
|
||||||
|
title: "T-Shirt Medusa",
|
||||||
|
description: "Un t-shirt en coton confortable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.id,
|
||||||
|
reference: "product",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: {
|
||||||
|
title: "Medusa T-Shirt DE",
|
||||||
|
description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Petit" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[0].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Klein" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "fr-FR",
|
||||||
|
translations: { title: "Moyen" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference_id: product.variants[1].id,
|
||||||
|
reference: "product_variant",
|
||||||
|
locale_code: "de-DE",
|
||||||
|
translations: { title: "Mittel" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
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
|
||||||
|
|
||||||
|
return (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Order creation from cart with locale", () => {
|
||||||
|
it("should preserve locale and translated items when order is created from cart", async () => {
|
||||||
|
const order = await createOrderFromCart("fr-FR")
|
||||||
|
|
||||||
|
expect(order.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "T-Shirt Medusa",
|
||||||
|
product_description: "Un t-shirt en coton confortable",
|
||||||
|
variant_title: "Petit",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have original values when order is created without locale", async () => {
|
||||||
|
const order = await createOrderFromCart()
|
||||||
|
|
||||||
|
expect(order.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt",
|
||||||
|
product_description: "A comfortable cotton t-shirt",
|
||||||
|
variant_title: "Small",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/orders/:id (update order locale)", () => {
|
||||||
|
it("should re-translate all items when locale is updated", async () => {
|
||||||
|
const order = await createOrderFromCart("fr-FR")
|
||||||
|
|
||||||
|
expect(order.items[0].variant_title).toEqual("Petit")
|
||||||
|
|
||||||
|
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.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "Medusa T-Shirt DE",
|
||||||
|
product_description: "Ein bequemes Baumwoll-T-Shirt",
|
||||||
|
variant_title: "Klein",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not re-translate items 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`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
expect(updatedOrder.email).toEqual("updated@example.com")
|
||||||
|
expect(updatedOrder.items[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
product_title: "T-Shirt Medusa",
|
||||||
|
variant_title: "Petit",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -286,6 +286,7 @@ medusaIntegrationTestRunner({
|
|||||||
payment_status: "not_paid",
|
payment_status: "not_paid",
|
||||||
region_id: "test_region_id",
|
region_id: "test_region_id",
|
||||||
fulfillments: [],
|
fulfillments: [],
|
||||||
|
locale: null,
|
||||||
metadata: {
|
metadata: {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export * from "./find-or-create-customer"
|
|||||||
export * from "./find-sales-channel"
|
export * from "./find-sales-channel"
|
||||||
export * from "./get-actions-to-compute-from-promotions"
|
export * from "./get-actions-to-compute-from-promotions"
|
||||||
export * from "./get-line-item-actions"
|
export * from "./get-line-item-actions"
|
||||||
export * from "./get-translated-line-items"
|
|
||||||
export * from "./update-cart-items-translations"
|
export * from "./update-cart-items-translations"
|
||||||
export * from "./get-promotion-codes-to-apply"
|
export * from "./get-promotion-codes-to-apply"
|
||||||
export * from "./get-variant-price-sets"
|
export * from "./get-variant-price-sets"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Modules,
|
Modules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
import { applyTranslationsToItems } from "../utils/apply-translations-to-items"
|
import { applyTranslationsToItems } from "../../common/utils/apply-translations-to-items"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
|
|
||||||
export interface UpdateCartItemsTranslationsStepInput {
|
export interface UpdateCartItemsTranslationsStepInput {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const completeCartFields = [
|
|||||||
"id",
|
"id",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
"email",
|
"email",
|
||||||
|
"locale",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"completed_at",
|
"completed_at",
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ import {
|
|||||||
when,
|
when,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { getTranslatedLineItemsStep, useQueryGraphStep } from "../../common"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
import { acquireLockStep, releaseLockStep } from "../../locking"
|
import { acquireLockStep, releaseLockStep } from "../../locking"
|
||||||
import {
|
import {
|
||||||
createLineItemsStep,
|
createLineItemsStep,
|
||||||
getLineItemActionsStep,
|
getLineItemActionsStep,
|
||||||
getTranslatedLineItemsStep,
|
|
||||||
updateLineItemsStep,
|
updateLineItemsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { validateCartStep } from "../steps/validate-cart"
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
|
|||||||
@@ -75,47 +75,47 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around completing a cart.
|
* You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around completing a cart.
|
||||||
* For example, in the [Subscriptions recipe](https://docs.medusajs.com/resources/recipes/subscriptions/examples/standard#create-workflow),
|
* For example, in the [Subscriptions recipe](https://docs.medusajs.com/resources/recipes/subscriptions/examples/standard#create-workflow),
|
||||||
* this workflow is used within another workflow that creates a subscription order.
|
* this workflow is used within another workflow that creates a subscription order.
|
||||||
*
|
*
|
||||||
* ## Cart Completion Idempotency
|
* ## Cart Completion Idempotency
|
||||||
*
|
*
|
||||||
* This workflow's logic is idempotent, meaning that if it is executed multiple times with the same input, it will not create duplicate orders. The
|
* This workflow's logic is idempotent, meaning that if it is executed multiple times with the same input, it will not create duplicate orders. The
|
||||||
* same order will be returned for subsequent executions with the same cart ID. This is necessary to avoid rolling back payments or causing
|
* same order will be returned for subsequent executions with the same cart ID. This is necessary to avoid rolling back payments or causing
|
||||||
* other side effects if the workflow is retried or fails due to transient errors.
|
* other side effects if the workflow is retried or fails due to transient errors.
|
||||||
*
|
*
|
||||||
* So, if you use this workflow within your own, make sure your workflow's steps are idempotent as well to avoid unintended side effects.
|
* So, if you use this workflow within your own, make sure your workflow's steps are idempotent as well to avoid unintended side effects.
|
||||||
* Your workflow must also acquire and release locks around this workflow to prevent concurrent executions for the same cart.
|
* Your workflow must also acquire and release locks around this workflow to prevent concurrent executions for the same cart.
|
||||||
*
|
*
|
||||||
* The following sections cover some common scenarios and how to handle them.
|
* The following sections cover some common scenarios and how to handle them.
|
||||||
*
|
*
|
||||||
* ### Creating Links and Linked Records
|
* ### Creating Links and Linked Records
|
||||||
*
|
*
|
||||||
* In some cases, you might want to create custom links or linked records to the order. For example, you might want to create a link from the order to a
|
* In some cases, you might want to create custom links or linked records to the order. For example, you might want to create a link from the order to a
|
||||||
* digital order.
|
* digital order.
|
||||||
*
|
*
|
||||||
* In such cases, ensure that your workflow's logic checks for existing links or records before creating new ones. You can query the
|
* In such cases, ensure that your workflow's logic checks for existing links or records before creating new ones. You can query the
|
||||||
* [entry point of the link](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns#method-2-using-entry-point)
|
* [entry point of the link](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns#method-2-using-entry-point)
|
||||||
* to check for existing links before creating new ones.
|
* to check for existing links before creating new ones.
|
||||||
*
|
*
|
||||||
* For example:
|
* For example:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import {
|
* import {
|
||||||
* createWorkflow,
|
* createWorkflow,
|
||||||
* when,
|
* when,
|
||||||
* WorkflowResponse
|
* WorkflowResponse
|
||||||
* } from "@medusajs/framework/workflows-sdk"
|
* } from "@medusajs/framework/workflows-sdk"
|
||||||
* import {
|
* import {
|
||||||
* useQueryGraphStep,
|
* useQueryGraphStep,
|
||||||
* completeCartWorkflow,
|
* completeCartWorkflow,
|
||||||
* acquireLockStep,
|
* acquireLockStep,
|
||||||
* releaseLockStep
|
* releaseLockStep
|
||||||
* } from "@medusajs/framework/workflows-sdk"
|
* } from "@medusajs/framework/workflows-sdk"
|
||||||
* import digitalProductOrderOrderLink from "../../links/digital-product-order"
|
* import digitalProductOrderOrderLink from "../../links/digital-product-order"
|
||||||
*
|
*
|
||||||
* type WorkflowInput = {
|
* type WorkflowInput = {
|
||||||
* cart_id: string
|
* cart_id: string
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* const createDigitalProductOrderWorkflow = createWorkflow(
|
* const createDigitalProductOrderWorkflow = createWorkflow(
|
||||||
* "create-digital-product-order",
|
* "create-digital-product-order",
|
||||||
* (input: WorkflowInput) => {
|
* (input: WorkflowInput) => {
|
||||||
@@ -129,14 +129,14 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* id: input.cart_id
|
* id: input.cart_id
|
||||||
* }
|
* }
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* const { data: existingLinks } = useQueryGraphStep({
|
* const { data: existingLinks } = useQueryGraphStep({
|
||||||
* entity: digitalProductOrderOrderLink.entryPoint,
|
* entity: digitalProductOrderOrderLink.entryPoint,
|
||||||
* fields: ["digital_product_order.id"],
|
* fields: ["digital_product_order.id"],
|
||||||
* filters: { order_id: id },
|
* filters: { order_id: id },
|
||||||
* }).config({ name: "retrieve-existing-links" });
|
* }).config({ name: "retrieve-existing-links" });
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* const digital_product_order = when(
|
* const digital_product_order = when(
|
||||||
* "create-digital-product-order-condition",
|
* "create-digital-product-order-condition",
|
||||||
* { existingLinks },
|
* { existingLinks },
|
||||||
@@ -149,60 +149,60 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* .then(() => {
|
* .then(() => {
|
||||||
* // create digital product order logic...
|
* // create digital product order logic...
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // other workflow logic...
|
* // other workflow logic...
|
||||||
*
|
*
|
||||||
* releaseLockStep({
|
* releaseLockStep({
|
||||||
* key: input.cart_id,
|
* key: input.cart_id,
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* return new WorkflowResponse({
|
* return new WorkflowResponse({
|
||||||
* // workflow output...
|
* // workflow output...
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ### Custom Validation with Conflicts
|
* ### Custom Validation with Conflicts
|
||||||
*
|
*
|
||||||
* Some use cases require custom validation that may cause conflicts on subsequent executions of the workflow.
|
* Some use cases require custom validation that may cause conflicts on subsequent executions of the workflow.
|
||||||
* For example, if you're selling tickets to an event, you might want to validate that the tickets are available
|
* For example, if you're selling tickets to an event, you might want to validate that the tickets are available
|
||||||
* on selected dates.
|
* on selected dates.
|
||||||
*
|
*
|
||||||
* In this scenario, if the workflow is retried after the first execution, the validation
|
* In this scenario, if the workflow is retried after the first execution, the validation
|
||||||
* will fail since the tickets would have already been reserved in the first execution. This makes the cart
|
* will fail since the tickets would have already been reserved in the first execution. This makes the cart
|
||||||
* completion non-idempotent.
|
* completion non-idempotent.
|
||||||
*
|
*
|
||||||
* To handle these cases, you can create a step that throws an error if the validation fails. Then, in the compensation function,
|
* To handle these cases, you can create a step that throws an error if the validation fails. Then, in the compensation function,
|
||||||
* you can cancel the order if the validation fails. For example:
|
* you can cancel the order if the validation fails. For example:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
* import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
* import { MedusaError } from "@medusajs/framework/utils"
|
* import { MedusaError } from "@medusajs/framework/utils"
|
||||||
* import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
|
* import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
|
||||||
*
|
*
|
||||||
* type StepInput = {
|
* type StepInput = {
|
||||||
* order_id: string
|
* order_id: string
|
||||||
* // other input fields...
|
* // other input fields...
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* export const customCartValidationStep = createStep(
|
* export const customCartValidationStep = createStep(
|
||||||
* "custom-cart-validation",
|
* "custom-cart-validation",
|
||||||
* async (input, { container }) => {
|
* async (input, { container }) => {
|
||||||
* const isValid = true // replace with actual validation logic
|
* const isValid = true // replace with actual validation logic
|
||||||
*
|
*
|
||||||
* if (!isValid) {
|
* if (!isValid) {
|
||||||
* throw new MedusaError(
|
* throw new MedusaError(
|
||||||
* MedusaError.Types.INVALID_DATA,
|
* MedusaError.Types.INVALID_DATA,
|
||||||
* "Custom cart validation failed"
|
* "Custom cart validation failed"
|
||||||
* )
|
* )
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* return new StepResponse(void 0, input.order_id)
|
* return new StepResponse(void 0, input.order_id)
|
||||||
* },
|
* },
|
||||||
* async (order_id, { container, context }) => {
|
* async (order_id, { container, context }) => {
|
||||||
* if (!order_id) return
|
* if (!order_id) return
|
||||||
*
|
*
|
||||||
* cancelOrderWorkflow(container).run({
|
* cancelOrderWorkflow(container).run({
|
||||||
* input: {
|
* input: {
|
||||||
* id: order_id,
|
* id: order_id,
|
||||||
@@ -213,10 +213,10 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Then, in your custom workflow, only run the validation step if the order is being created for the first time. For example,
|
* Then, in your custom workflow, only run the validation step if the order is being created for the first time. For example,
|
||||||
* only run the validation if the link from the order to your custom data does not exist yet:
|
* only run the validation if the link from the order to your custom data does not exist yet:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* import {
|
* import {
|
||||||
* createWorkflow,
|
* createWorkflow,
|
||||||
@@ -225,11 +225,11 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* } from "@medusajs/framework/workflows-sdk"
|
* } from "@medusajs/framework/workflows-sdk"
|
||||||
* import { useQueryGraphStep } from "@medusajs/framework/workflows-sdk"
|
* import { useQueryGraphStep } from "@medusajs/framework/workflows-sdk"
|
||||||
* import ticketOrderLink from "../../links/ticket-order"
|
* import ticketOrderLink from "../../links/ticket-order"
|
||||||
*
|
*
|
||||||
* type WorkflowInput = {
|
* type WorkflowInput = {
|
||||||
* cart_id: string
|
* cart_id: string
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* const createTicketOrderWorkflow = createWorkflow(
|
* const createTicketOrderWorkflow = createWorkflow(
|
||||||
* "create-ticket-order",
|
* "create-ticket-order",
|
||||||
* (input: WorkflowInput) => {
|
* (input: WorkflowInput) => {
|
||||||
@@ -243,14 +243,14 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* id: input.cart_id
|
* id: input.cart_id
|
||||||
* }
|
* }
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* const { data: existingLinks } = useQueryGraphStep({
|
* const { data: existingLinks } = useQueryGraphStep({
|
||||||
* entity: ticketOrderLink.entryPoint,
|
* entity: ticketOrderLink.entryPoint,
|
||||||
* fields: ["ticket.id"],
|
* fields: ["ticket.id"],
|
||||||
* filters: { order_id: id },
|
* filters: { order_id: id },
|
||||||
* }).config({ name: "retrieve-existing-links" });
|
* }).config({ name: "retrieve-existing-links" });
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* const ticket_order = when(
|
* const ticket_order = when(
|
||||||
* "create-ticket-order-condition",
|
* "create-ticket-order-condition",
|
||||||
* { existingLinks },
|
* { existingLinks },
|
||||||
@@ -264,23 +264,23 @@ export const completeCartWorkflowId = "complete-cart"
|
|||||||
* customCartValidationStep({ order_id: id })
|
* customCartValidationStep({ order_id: id })
|
||||||
* // create ticket order logic...
|
* // create ticket order logic...
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* // other workflow logic...
|
* // other workflow logic...
|
||||||
*
|
*
|
||||||
* releaseLockStep({
|
* releaseLockStep({
|
||||||
* key: input.cart_id,
|
* key: input.cart_id,
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* return new WorkflowResponse({
|
* return new WorkflowResponse({
|
||||||
* // workflow output...
|
* // workflow output...
|
||||||
* })
|
* })
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* The first time this workflow is executed for a cart, the validation step will run and validate the cart. If the validation fails,
|
* The first time this workflow is executed for a cart, the validation step will run and validate the cart. If the validation fails,
|
||||||
* the order will be canceled in the compensation function.
|
* the order will be canceled in the compensation function.
|
||||||
*
|
*
|
||||||
* If the validation is successful and the workflow is retried, the validation step will be skipped since the link from the order to the
|
* If the validation is successful and the workflow is retried, the validation step will be skipped since the link from the order to the
|
||||||
* ticket order already exists. This ensures that the workflow remains idempotent.
|
* ticket order already exists. This ensures that the workflow remains idempotent.
|
||||||
*
|
*
|
||||||
@@ -472,6 +472,7 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
status: OrderStatus.PENDING,
|
status: OrderStatus.PENDING,
|
||||||
email: cart.email,
|
email: cart.email,
|
||||||
currency_code: cart.currency_code,
|
currency_code: cart.currency_code,
|
||||||
|
locale: cart.locale,
|
||||||
shipping_address: shippingAddress,
|
shipping_address: shippingAddress,
|
||||||
billing_address: billingAddress,
|
billing_address: billingAddress,
|
||||||
no_notification: false,
|
no_notification: false,
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
findOneOrAnyRegionStep,
|
findOneOrAnyRegionStep,
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
findSalesChannelStep,
|
findSalesChannelStep,
|
||||||
getTranslatedLineItemsStep,
|
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
|
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
@@ -35,6 +34,7 @@ import { getVariantsAndItemsWithPrices } from "./get-variants-and-items-with-pri
|
|||||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||||
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
||||||
|
import { getTranslatedLineItemsStep } from "../../common"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data to create the cart, along with custom data that's passed to the workflow's hooks.
|
* The data to create the cart, along with custom data that's passed to the workflow's hooks.
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from "./workflows/batch-links"
|
|||||||
export * from "./workflows/create-links"
|
export * from "./workflows/create-links"
|
||||||
export * from "./workflows/dismiss-links"
|
export * from "./workflows/dismiss-links"
|
||||||
export * from "./workflows/update-links"
|
export * from "./workflows/update-links"
|
||||||
|
export * from "./steps/get-translated-line-items"
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import { applyTranslationsToItems } from "../utils/apply-translations-to-items"
|
|||||||
export interface GetTranslatedLineItemsStepInput<T> {
|
export interface GetTranslatedLineItemsStepInput<T> {
|
||||||
items: T[] | undefined
|
items: T[] | undefined
|
||||||
variants: Partial<ProductVariantDTO>[]
|
variants: Partial<ProductVariantDTO>[]
|
||||||
locale: string | undefined
|
locale: string | null | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTranslatedLineItemsStepId = "get-translated-line-items"
|
export const getTranslatedLineItemsStepId = "get-translated-line-items"
|
||||||
File renamed without changes.
@@ -1,12 +1,3 @@
|
|||||||
import { Modules, OrderWorkflowEvents } from "@medusajs/framework/utils"
|
|
||||||
import {
|
|
||||||
createStep,
|
|
||||||
createWorkflow,
|
|
||||||
StepResponse,
|
|
||||||
transform,
|
|
||||||
WorkflowData,
|
|
||||||
WorkflowResponse,
|
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
|
||||||
import {
|
import {
|
||||||
IOrderModuleService,
|
IOrderModuleService,
|
||||||
OrderDTO,
|
OrderDTO,
|
||||||
@@ -14,10 +5,24 @@ import {
|
|||||||
UpdateOrderDTO,
|
UpdateOrderDTO,
|
||||||
UpsertOrderAddressDTO,
|
UpsertOrderAddressDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
|
import { Modules, OrderWorkflowEvents } from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
StepResponse,
|
||||||
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||||
import { previewOrderChangeStep, registerOrderChangesStep } from "../../order"
|
|
||||||
import { validateDraftOrderStep } from "../steps/validate-draft-order"
|
|
||||||
import { acquireLockStep, releaseLockStep } from "../../locking"
|
import { acquireLockStep, releaseLockStep } from "../../locking"
|
||||||
|
import {
|
||||||
|
previewOrderChangeStep,
|
||||||
|
registerOrderChangesStep,
|
||||||
|
updateOrderItemsTranslationsStep,
|
||||||
|
} from "../../order"
|
||||||
|
import { validateDraftOrderStep } from "../steps/validate-draft-order"
|
||||||
|
|
||||||
export const updateDraftOrderWorkflowId = "update-draft-order"
|
export const updateDraftOrderWorkflowId = "update-draft-order"
|
||||||
|
|
||||||
@@ -53,6 +58,11 @@ export interface UpdateDraftOrderWorkflowInput {
|
|||||||
* The ID of the sales channel to associate the draft order with.
|
* The ID of the sales channel to associate the draft order with.
|
||||||
*/
|
*/
|
||||||
sales_channel_id?: string
|
sales_channel_id?: string
|
||||||
|
/**
|
||||||
|
* The new locale of the draft order. When changed, all line items
|
||||||
|
* will be re-translated to the new locale.
|
||||||
|
*/
|
||||||
|
locale?: string | null
|
||||||
/**
|
/**
|
||||||
* The new metadata of the draft order.
|
* The new metadata of the draft order.
|
||||||
*/
|
*/
|
||||||
@@ -166,6 +176,7 @@ export const updateDraftOrderWorkflow = createWorkflow(
|
|||||||
"sales_channel_id",
|
"sales_channel_id",
|
||||||
"email",
|
"email",
|
||||||
"customer_id",
|
"customer_id",
|
||||||
|
"locale",
|
||||||
"shipping_address.*",
|
"shipping_address.*",
|
||||||
"billing_address.*",
|
"billing_address.*",
|
||||||
"metadata",
|
"metadata",
|
||||||
@@ -306,12 +317,35 @@ export const updateDraftOrderWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!!input.locale && input.locale !== order.locale) {
|
||||||
|
changes.push({
|
||||||
|
change_type: "update_order" as const,
|
||||||
|
order_id: input.id,
|
||||||
|
created_by: input.user_id,
|
||||||
|
confirmed_by: input.user_id,
|
||||||
|
details: {
|
||||||
|
type: "locale",
|
||||||
|
old: order.locale,
|
||||||
|
new: updatedOrder.locale,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
registerOrderChangesStep(orderChangeInput)
|
registerOrderChangesStep(orderChangeInput)
|
||||||
|
|
||||||
|
when({ input, order }, ({ input, order }) => {
|
||||||
|
return !!input.locale && input.locale !== order.locale
|
||||||
|
}).then(() => {
|
||||||
|
updateOrderItemsTranslationsStep({
|
||||||
|
order_id: input.id,
|
||||||
|
locale: input.locale!,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: OrderWorkflowEvents.UPDATED,
|
eventName: OrderWorkflowEvents.UPDATED,
|
||||||
data: { id: input.id },
|
data: { id: input.id },
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ export * from "./return/update-returns"
|
|||||||
export * from "./set-tax-lines-for-items"
|
export * from "./set-tax-lines-for-items"
|
||||||
export * from "./update-order-change-actions"
|
export * from "./update-order-change-actions"
|
||||||
export * from "./update-order-changes"
|
export * from "./update-order-changes"
|
||||||
|
export * from "./update-order-items-translations"
|
||||||
export * from "./update-orders"
|
export * from "./update-orders"
|
||||||
export * from "./update-shipping-methods"
|
export * from "./update-shipping-methods"
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { MedusaContainer } from "@medusajs/framework"
|
||||||
|
import {
|
||||||
|
IOrderModuleService,
|
||||||
|
ProductVariantDTO,
|
||||||
|
RemoteQueryFunction,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
applyTranslations,
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
deduplicate,
|
||||||
|
FeatureFlag,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { applyTranslationsToItems } from "../../common/utils/apply-translations-to-items"
|
||||||
|
import { productVariantsFields } from "../utils/fields"
|
||||||
|
|
||||||
|
export interface UpdateOrderItemsTranslationsStepInput {
|
||||||
|
order_id: string
|
||||||
|
locale: string
|
||||||
|
/**
|
||||||
|
* Pre-loaded items to avoid re-fetching.
|
||||||
|
*/
|
||||||
|
items?: { id: string; variant_id?: string; [key: string]: any }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const BATCH_SIZE = 100
|
||||||
|
|
||||||
|
const lineItemFields = [
|
||||||
|
"id",
|
||||||
|
"variant_id",
|
||||||
|
"product_id",
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"product_title",
|
||||||
|
"product_description",
|
||||||
|
"product_subtitle",
|
||||||
|
"product_type",
|
||||||
|
"product_collection",
|
||||||
|
"product_handle",
|
||||||
|
"variant_title",
|
||||||
|
]
|
||||||
|
|
||||||
|
export const updateOrderItemsTranslationsStepId =
|
||||||
|
"update-order-items-translations"
|
||||||
|
|
||||||
|
type ItemTranslationSnapshot = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
product_title: string
|
||||||
|
product_description: string
|
||||||
|
product_subtitle: string
|
||||||
|
product_type: string
|
||||||
|
product_collection: string
|
||||||
|
product_handle: string
|
||||||
|
variant_title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compensation(
|
||||||
|
originalItems: ItemTranslationSnapshot[] | undefined,
|
||||||
|
{ container }: { container: MedusaContainer }
|
||||||
|
) {
|
||||||
|
if (!originalItems?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderModule = container.resolve<IOrderModuleService>(Modules.ORDER)
|
||||||
|
|
||||||
|
for (let i = 0; i < originalItems.length; i += BATCH_SIZE) {
|
||||||
|
const batch = originalItems.slice(i, i + BATCH_SIZE)
|
||||||
|
await orderModule.updateOrderLineItems(
|
||||||
|
batch.map((item) => ({
|
||||||
|
selector: { id: item.id },
|
||||||
|
data: {
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
product_title: item.product_title,
|
||||||
|
product_description: item.product_description,
|
||||||
|
product_subtitle: item.product_subtitle,
|
||||||
|
product_type: item.product_type,
|
||||||
|
product_collection: item.product_collection,
|
||||||
|
product_handle: item.product_handle,
|
||||||
|
variant_title: item.variant_title,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step re-translates all order line items when the order's locale changes.
|
||||||
|
* It fetches items and their variants in batches to handle large orders gracefully.
|
||||||
|
*/
|
||||||
|
export const updateOrderItemsTranslationsStep = createStep(
|
||||||
|
updateOrderItemsTranslationsStepId,
|
||||||
|
async (data: UpdateOrderItemsTranslationsStepInput, { container }) => {
|
||||||
|
const originalItems: ItemTranslationSnapshot[] = []
|
||||||
|
try {
|
||||||
|
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
|
||||||
|
|
||||||
|
if (!isTranslationEnabled || !data.locale) {
|
||||||
|
return new StepResponse(void 0, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderModule = container.resolve<IOrderModuleService>(Modules.ORDER)
|
||||||
|
const query = container.resolve<RemoteQueryFunction>(
|
||||||
|
ContainerRegistrationKeys.QUERY
|
||||||
|
)
|
||||||
|
|
||||||
|
const processBatch = async (
|
||||||
|
items: { id: string; variant_id?: string; [key: string]: any }[]
|
||||||
|
) => {
|
||||||
|
const variantIds = deduplicate(
|
||||||
|
items
|
||||||
|
.map((item) => item.variant_id)
|
||||||
|
.filter((id): id is string => !!id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (variantIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store original values before updating
|
||||||
|
for (const item of items) {
|
||||||
|
originalItems.push({
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
product_title: item.product_title,
|
||||||
|
product_description: item.product_description,
|
||||||
|
product_subtitle: item.product_subtitle,
|
||||||
|
product_type: item.product_type,
|
||||||
|
product_collection: item.product_collection,
|
||||||
|
product_handle: item.product_handle,
|
||||||
|
variant_title: item.variant_title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: variants } = await query.graph({
|
||||||
|
entity: "variants",
|
||||||
|
filters: { id: variantIds },
|
||||||
|
fields: productVariantsFields,
|
||||||
|
})
|
||||||
|
|
||||||
|
await applyTranslations({
|
||||||
|
localeCode: data.locale,
|
||||||
|
objects: variants as Record<string, any>[],
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
|
||||||
|
const translatedItems = applyTranslationsToItems(
|
||||||
|
items as { variant_id?: string; [key: string]: any }[],
|
||||||
|
variants as Partial<ProductVariantDTO>[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemsToUpdate = translatedItems
|
||||||
|
.filter((item) => item.id)
|
||||||
|
.map((item) => ({
|
||||||
|
selector: { id: item.id },
|
||||||
|
data: {
|
||||||
|
title: item.title,
|
||||||
|
subtitle: item.subtitle,
|
||||||
|
product_title: item.product_title,
|
||||||
|
product_description: item.product_description,
|
||||||
|
product_subtitle: item.product_subtitle,
|
||||||
|
product_type: item.product_type,
|
||||||
|
product_collection: item.product_collection,
|
||||||
|
product_handle: item.product_handle,
|
||||||
|
variant_title: item.variant_title,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (itemsToUpdate.length > 0) {
|
||||||
|
await orderModule.updateOrderLineItems(itemsToUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.items?.length) {
|
||||||
|
await processBatch(data.items)
|
||||||
|
return new StepResponse(void 0, originalItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: orders } = await query.graph({
|
||||||
|
entity: "orders",
|
||||||
|
filters: { id: data.order_id },
|
||||||
|
fields: lineItemFields.map((f) => `items.${f}`),
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderData = orders[0] as {
|
||||||
|
items?: { id: string; variant_id?: string }[]
|
||||||
|
}
|
||||||
|
const items = orderData?.items ?? []
|
||||||
|
|
||||||
|
// Process items in batches
|
||||||
|
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
||||||
|
const batch = items.slice(i, i + BATCH_SIZE)
|
||||||
|
await processBatch(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse(void 0, originalItems)
|
||||||
|
} catch (error) {
|
||||||
|
await compensation(originalItems, { container })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compensation
|
||||||
|
)
|
||||||
@@ -21,7 +21,7 @@ import { requiredVariantFieldsForInventoryConfirmation } from "../../cart/utils/
|
|||||||
import { pricingContextResult } from "../../cart/utils/schemas"
|
import { pricingContextResult } from "../../cart/utils/schemas"
|
||||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
import { getVariantsAndItemsWithPrices } from "../../cart/workflows/get-variants-and-items-with-prices"
|
import { getVariantsAndItemsWithPrices } from "../../cart/workflows/get-variants-and-items-with-prices"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { getTranslatedLineItemsStep, useQueryGraphStep } from "../../common"
|
||||||
import { createOrderLineItemsStep } from "../steps"
|
import { createOrderLineItemsStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
|
|
||||||
@@ -108,6 +108,7 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
"customer_id",
|
"customer_id",
|
||||||
"email",
|
"email",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
|
"locale",
|
||||||
],
|
],
|
||||||
options: { throwIfKeyNotFound: true, isList: false },
|
options: { throwIfKeyNotFound: true, isList: false },
|
||||||
}).config({ name: "order-query" })
|
}).config({ name: "order-query" })
|
||||||
@@ -176,9 +177,15 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const translatedItems = getTranslatedLineItemsStep({
|
||||||
|
items,
|
||||||
|
variants,
|
||||||
|
locale: order.locale,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(
|
return new WorkflowResponse(
|
||||||
createOrderLineItemsStep({
|
createOrderLineItemsStep({
|
||||||
items: items,
|
items: translatedItems,
|
||||||
}) satisfies OrderAddLineItemWorkflowOutput,
|
}) satisfies OrderAddLineItemWorkflowOutput,
|
||||||
{
|
{
|
||||||
hooks: [setPricingContext] as const,
|
hooks: [setPricingContext] as const,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { OrderDTO, OrderWorkflow } from "@medusajs/framework/types"
|
import type { OrderDTO, OrderWorkflow } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
OrderPreviewDTO,
|
||||||
|
RegisterOrderChangeDTO,
|
||||||
|
UpdateOrderDTO,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
MedusaError,
|
MedusaError,
|
||||||
OrderWorkflowEvents,
|
OrderWorkflowEvents,
|
||||||
@@ -10,17 +15,14 @@ import {
|
|||||||
createStep,
|
createStep,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
|
||||||
OrderPreviewDTO,
|
|
||||||
RegisterOrderChangeDTO,
|
|
||||||
UpdateOrderDTO,
|
|
||||||
} from "@medusajs/framework/types"
|
|
||||||
|
|
||||||
import { emitEventStep, useQueryGraphStep } from "../../common"
|
import { emitEventStep, useQueryGraphStep } from "../../common"
|
||||||
import {
|
import {
|
||||||
previewOrderChangeStep,
|
previewOrderChangeStep,
|
||||||
registerOrderChangesStep,
|
registerOrderChangesStep,
|
||||||
|
updateOrderItemsTranslationsStep,
|
||||||
updateOrdersStep,
|
updateOrdersStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { throwIfOrderIsCancelled } from "../utils/order-validation"
|
import { throwIfOrderIsCancelled } from "../utils/order-validation"
|
||||||
@@ -128,6 +130,7 @@ export const updateOrderWorkflow = createWorkflow(
|
|||||||
"id",
|
"id",
|
||||||
"status",
|
"status",
|
||||||
"email",
|
"email",
|
||||||
|
"locale",
|
||||||
"shipping_address.*",
|
"shipping_address.*",
|
||||||
"billing_address.*",
|
"billing_address.*",
|
||||||
"metadata",
|
"metadata",
|
||||||
@@ -235,12 +238,35 @@ export const updateOrderWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!!input.locale && input.locale !== order.locale) {
|
||||||
|
changes.push({
|
||||||
|
change_type: "update_order" as const,
|
||||||
|
order_id: input.id,
|
||||||
|
created_by: input.user_id,
|
||||||
|
confirmed_by: input.user_id,
|
||||||
|
details: {
|
||||||
|
type: "locale",
|
||||||
|
old: order.locale,
|
||||||
|
new: input.locale,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
registerOrderChangesStep(orderChangeInput)
|
registerOrderChangesStep(orderChangeInput)
|
||||||
|
|
||||||
|
when("locale-changed", { input, order }, ({ input, order }) => {
|
||||||
|
return !!input.locale && input.locale !== order.locale
|
||||||
|
}).then(() => {
|
||||||
|
updateOrderItemsTranslationsStep({
|
||||||
|
order_id: input.id,
|
||||||
|
locale: input.locale!,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: OrderWorkflowEvents.UPDATED,
|
eventName: OrderWorkflowEvents.UPDATED,
|
||||||
data: { id: input.id },
|
data: { id: input.id },
|
||||||
|
|||||||
@@ -1133,6 +1133,11 @@ export interface OrderDTO {
|
|||||||
*/
|
*/
|
||||||
is_draft_order?: boolean
|
is_draft_order?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The locale of the order.
|
||||||
|
*/
|
||||||
|
locale?: string | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds custom data in key-value pairs.
|
* Holds custom data in key-value pairs.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -138,6 +138,11 @@ export interface CreateOrderDTO {
|
|||||||
*/
|
*/
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The locale of the order.
|
||||||
|
*/
|
||||||
|
locale?: string | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The associated shipping address's ID.
|
* The associated shipping address's ID.
|
||||||
*/
|
*/
|
||||||
@@ -234,6 +239,11 @@ export interface UpdateOrderDTO {
|
|||||||
*/
|
*/
|
||||||
is_draft_order?: boolean
|
is_draft_order?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The locale of the order.
|
||||||
|
*/
|
||||||
|
locale?: string | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The items of the order.
|
* The items of the order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export type UpdateOrderWorkflowInput = {
|
|||||||
* The new email of the order.
|
* The new email of the order.
|
||||||
*/
|
*/
|
||||||
email?: string
|
email?: string
|
||||||
|
/**
|
||||||
|
* The new locale of the order. When changed, all line items
|
||||||
|
* will be re-translated to the new locale.
|
||||||
|
*/
|
||||||
|
locale?: string | null
|
||||||
/**
|
/**
|
||||||
* The new metadata of the order.
|
* The new metadata of the order.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const CreateDraftOrder = z
|
|||||||
currency_code: z.string().nullish(),
|
currency_code: z.string().nullish(),
|
||||||
no_notification_order: z.boolean().optional(),
|
no_notification_order: z.boolean().optional(),
|
||||||
shipping_methods: z.array(ShippingMethod).optional(),
|
shipping_methods: z.array(ShippingMethod).optional(),
|
||||||
|
locale: z.string().optional(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
@@ -111,6 +112,7 @@ export const AdminUpdateDraftOrder = z.object({
|
|||||||
shipping_address: AddressPayload.optional(),
|
shipping_address: AddressPayload.optional(),
|
||||||
billing_address: AddressPayload.optional(),
|
billing_address: AddressPayload.optional(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
|
locale: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AdminAddDraftOrderPromotionsType = z.infer<
|
export type AdminAddDraftOrderPromotionsType = z.infer<
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const defaultAdminOrderFields = [
|
|||||||
"summary",
|
"summary",
|
||||||
"total",
|
"total",
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"locale",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,15 +9,12 @@ import {
|
|||||||
|
|
||||||
export const AdminGetOrdersOrderParams = createSelectParams().merge(
|
export const AdminGetOrdersOrderParams = createSelectParams().merge(
|
||||||
z.object({
|
z.object({
|
||||||
version: z.preprocess(
|
version: z.preprocess((val) => {
|
||||||
(val) => {
|
if (val && typeof val === "string") {
|
||||||
if (val && typeof val === "string") {
|
return parseInt(val)
|
||||||
return parseInt(val)
|
}
|
||||||
}
|
return val
|
||||||
return val
|
}, z.number().optional()),
|
||||||
},
|
|
||||||
z.number().optional()
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,6 +148,7 @@ export const AdminUpdateOrder = z.object({
|
|||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
shipping_address: AddressPayload.optional(),
|
shipping_address: AddressPayload.optional(),
|
||||||
billing_address: AddressPayload.optional(),
|
billing_address: AddressPayload.optional(),
|
||||||
|
locale: z.string().nullish(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const defaultStoreCartFields = [
|
|||||||
"id",
|
"id",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
"email",
|
"email",
|
||||||
|
"locale",
|
||||||
"region_id",
|
"region_id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|||||||
@@ -304,6 +304,15 @@
|
|||||||
"nullable": false,
|
"nullable": false,
|
||||||
"mappedType": "text"
|
"mappedType": "text"
|
||||||
},
|
},
|
||||||
|
"locale": {
|
||||||
|
"name": "locale",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": true,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
"no_notification": {
|
"no_notification": {
|
||||||
"name": "no_notification",
|
"name": "no_notification",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20251210112909 extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`alter table if exists "order" add column if not exists "locale" text null;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`alter table if exists "order" drop column if exists "locale";`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ const _Order = model
|
|||||||
is_draft_order: model.boolean().default(false),
|
is_draft_order: model.boolean().default(false),
|
||||||
email: model.text().searchable().nullable(),
|
email: model.text().searchable().nullable(),
|
||||||
currency_code: model.text(),
|
currency_code: model.text(),
|
||||||
|
locale: model.text().nullable(),
|
||||||
no_notification: model.boolean().nullable(),
|
no_notification: model.boolean().nullable(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
canceled_at: model.dateTime().nullable(),
|
canceled_at: model.dateTime().nullable(),
|
||||||
|
|||||||
Reference in new issue
Block a user