feat(): sync cart translation synced (#14226)

ref: https://github.com/medusajs/medusa/pull/14189

  **Summary**

  This PR extends the translation module to support automatic translation syncing for cart line items based on the cart's locale.

  Key changes:
  - Added locale field to the Cart model to store the cart's locale preference
  - Created new workflow steps:
    - getTranslatedLineItemsStep - Translates line items when adding to cart or creating a cart
    - updateCartItemsTranslationsStep - Re-translates all cart items when the cart's locale changes
  - Integrated translation logic into cart workflows:
    - createCartWorkflow - Applies translations to initial line items
    - addToCartWorkflow - Applies translations when adding new items
    - updateCartWorkflow - Re-translates all items when locale_code is updated
    - refreshCartItemsWorkflow - Maintains translations during cart refresh
  - Added applyTranslationsToItems utility to map variant/product/type/collection translations to line item fields (title, subtitle, description, etc.)
This commit is contained in:
Adrien de Peretti
2025-12-10 09:37:30 +01:00
committed by GitHub
parent 356283c359
commit e4877616c3
31 changed files with 2635 additions and 1474 deletions

View File

@@ -0,0 +1,9 @@
---
"@medusajs/medusa": patch
"@medusajs/test-utils": patch
"@medusajs/cart": patch
"@medusajs/core-flows": patch
"@medusajs/types": patch
---
feat(): sync cart translation synced

View File

@@ -0,0 +1,520 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules, ProductStatus } from "@medusajs/utils"
import {
createAdminUser,
generatePublishableKey,
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
import { MedusaContainer } from "@medusajs/types"
jest.setTimeout(100000)
process.env.MEDUSA_FF_TRANSLATION = "true"
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
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("Store Cart 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 }
beforeAll(async () => {
appContainer = getContainer()
})
afterAll(async () => {
delete process.env.MEDUSA_FF_TRANSLATION
})
beforeEach(async () => {
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" },
],
})
shippingProfile = (
await api.post(
`/admin/shipping-profiles`,
{ name: "default", type: "default" },
adminHeaders
)
).data.shipping_profile
region = (
await api.post(
"/admin/regions",
{ name: "US", currency_code: "usd", countries: ["us"] },
adminHeaders
)
).data.region
// 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" },
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
// 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!]
// 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
)
})
describe("POST /store/carts (create cart with locale)", () => {
it("should create a cart with translated items when locale is provided", async () => {
const response = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart).toEqual(
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
product_title: "T-Shirt Medusa",
product_description: "Un t-shirt en coton confortable",
variant_title: "Petit",
}),
]),
})
)
})
it("should create a cart with original values when no locale is provided", async () => {
const response = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
shipping_address: shippingAddressData,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart.items[0]).toEqual(
expect.objectContaining({
product_title: "Medusa T-Shirt",
product_description: "A comfortable cotton t-shirt",
variant_title: "Small",
})
)
})
})
describe("POST /store/carts/:id/line-items (add items to cart)", () => {
it("should translate new items using the cart's locale", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
},
storeHeaders
)
const cart = cartResponse.data.cart
const addItemResponse = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
},
storeHeaders
)
expect(addItemResponse.status).toEqual(200)
expect(addItemResponse.data.cart.items[0]).toEqual(
expect.objectContaining({
product_title: "T-Shirt Medusa",
product_description: "Un t-shirt en coton confortable",
variant_title: "Petit",
})
)
})
it("should translate multiple items added to cart", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
},
storeHeaders
)
const cart = cartResponse.data.cart
await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[0].id,
quantity: 1,
},
storeHeaders
)
const response = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[1].id,
quantity: 2,
},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.cart.items).toHaveLength(2)
const smallItem = response.data.cart.items.find(
(item) => item.variant_id === product.variants[0].id
)
const mediumItem = response.data.cart.items.find(
(item) => item.variant_id === product.variants[1].id
)
expect(smallItem).toEqual(
expect.objectContaining({
product_title: "T-Shirt Medusa",
variant_title: "Petit",
})
)
expect(mediumItem).toEqual(
expect.objectContaining({
product_title: "T-Shirt Medusa",
variant_title: "Moyen",
})
)
})
})
describe("POST /store/carts/:id (update cart locale)", () => {
it("should re-translate all items when locale is updated", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
items: [
{ variant_id: product.variants[0].id, quantity: 1 },
{ variant_id: product.variants[1].id, quantity: 1 },
],
},
storeHeaders
)
const cart = cartResponse.data.cart
const frenchSmallItem = cart.items.find(
(item) => item.variant_id === product.variants[0].id
)
expect(frenchSmallItem.variant_title).toEqual("Petit")
const updateResponse = await api.post(
`/store/carts/${cart.id}`,
{
locale: "de-DE",
},
storeHeaders
)
expect(updateResponse.status).toEqual(200)
const updatedCartResponse = await api.get(
`/store/carts/${cart.id}`,
storeHeaders
)
const updatedCart = updatedCartResponse.data.cart
const germanSmallItem = updatedCart.items.find(
(item) => item.variant_id === product.variants[0].id
)
const germanMediumItem = updatedCart.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",
})
)
})
it("should not re-translate items when locale is not changed", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeaders
)
const cart = cartResponse.data.cart
const updateResponse = await api.post(
`/store/carts/${cart.id}`,
{
email: "test@example.com",
},
storeHeaders
)
expect(updateResponse.status).toEqual(200)
const updatedCartResponse = await api.get(
`/store/carts/${cart.id}`,
storeHeaders
)
const updatedCart = updatedCartResponse.data.cart
expect(updatedCart.items[0]).toEqual(
expect.objectContaining({
product_title: "T-Shirt Medusa",
variant_title: "Petit",
})
)
})
it("should handle updating to a locale with no translations", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeaders
)
const cart = cartResponse.data.cart
const updateResponse = await api.post(
`/store/carts/${cart.id}`,
{
locale: "ja-JP",
},
storeHeaders
)
expect(updateResponse.status).toEqual(200)
// Fetch updated cart - should have original values since no Japanese translation exists
const updatedCartResponse = await api.get(
`/store/carts/${cart.id}`,
storeHeaders
)
// no translation means it will revert to default values
const updatedCart = updatedCartResponse.data.cart
expect(updatedCart.items[0]).toEqual(
expect.objectContaining({
product_title: "Medusa T-Shirt",
variant_title: "Small",
})
)
})
})
describe("Cart with items and locale changes", () => {
it("should maintain translations when adding items to a cart with existing locale", async () => {
const cartResponse = await api.post(
`/store/carts`,
{
currency_code: "usd",
sales_channel_id: salesChannel.id,
region_id: region.id,
locale: "fr-FR",
shipping_address: shippingAddressData,
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
},
storeHeaders
)
const cart = cartResponse.data.cart
const addResponse = await api.post(
`/store/carts/${cart.id}/line-items`,
{
variant_id: product.variants[1].id,
quantity: 1,
},
storeHeaders
)
expect(addResponse.data.cart.items).toHaveLength(2)
const allItemsTranslated = addResponse.data.cart.items.every(
(item) => item.product_title === "T-Shirt Medusa"
)
expect(allItemsTranslated).toBe(true)
})
})
})
},
})

View File

@@ -21,6 +21,69 @@ const customFulfillmentProviderCalculated = {
id: "test-provider-calculated",
}
const translationModuleResolutions =
process.env.MEDUSA_FF_TRANSLATION === "true"
? {
[Modules.TRANSLATION]: {
resolve: "@medusajs/translation",
},
}
: {}
const modules = {
...translationModuleResolutions,
[Modules.FULFILLMENT]: {
/** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */
options: {
providers: [
customFulfillmentProvider,
customFulfillmentProviderCalculated,
],
},
},
[Modules.NOTIFICATION]: {
resolve: "@medusajs/notification",
options: {
providers: [
{
resolve: "@medusajs/notification-local",
id: "local",
options: {
name: "Local Notification Provider",
channels: ["feed"],
},
},
],
},
},
[Modules.FILE]: {
resolve: "@medusajs/file",
options: {
providers: [
{
resolve: "@medusajs/file-local",
id: "local",
options: {
// This is the directory where we can reliably write in CI environments
upload_dir: path.join(os.tmpdir(), "uploads"),
private_upload_dir: path.join(os.tmpdir(), "static"),
},
},
],
},
},
[Modules.INDEX]: {
resolve: "@medusajs/index",
disable: process.env.ENABLE_INDEX_MODULE !== "true",
},
}
if (process.env.MEDUSA_FF_TRANSLATION === "true") {
modules[Modules.TRANSLATION] = {
resolve: "@medusajs/translation",
}
}
module.exports = defineConfig({
admin: {
disable: true,
@@ -33,50 +96,5 @@ module.exports = defineConfig({
featureFlags: {
index_engine: process.env.ENABLE_INDEX_MODULE === "true",
},
modules: {
[Modules.FULFILLMENT]: {
/** @type {import('@medusajs/fulfillment').FulfillmentModuleOptions} */
options: {
providers: [
customFulfillmentProvider,
customFulfillmentProviderCalculated,
],
},
},
[Modules.NOTIFICATION]: {
resolve: "@medusajs/notification",
options: {
providers: [
{
resolve: "@medusajs/notification-local",
id: "local",
options: {
name: "Local Notification Provider",
channels: ["feed"],
},
},
],
},
},
[Modules.FILE]: {
resolve: "@medusajs/file",
options: {
providers: [
{
resolve: "@medusajs/file-local",
id: "local",
options: {
// This is the directory where we can reliably write in CI environments
upload_dir: path.join(os.tmpdir(), "uploads"),
private_upload_dir: path.join(os.tmpdir(), "static"),
},
},
],
},
},
[Modules.INDEX]: {
resolve: "@medusajs/index",
disable: process.env.ENABLE_INDEX_MODULE !== "true",
},
},
modules,
})

View File

@@ -1360,6 +1360,7 @@ medusaIntegrationTestRunner({
id: expect.stringContaining("cart_"),
sales_channel_id: expect.stringContaining("sc_"),
currency_code: "usd",
locale: null,
region_id: expect.stringContaining("reg_"),
shipping_address: null,
item_total: 0,

View File

@@ -0,0 +1,46 @@
import { ProductVariantDTO } from "@medusajs/framework/types"
import { applyTranslations, FeatureFlag } from "@medusajs/framework/utils"
import {
createStep,
StepFunction,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { applyTranslationsToItems } from "../utils/apply-translations-to-items"
export interface GetTranslatedLineItemsStepInput<T> {
items: T[] | undefined
variants: Partial<ProductVariantDTO>[]
locale: string | undefined
}
export const getTranslatedLineItemsStepId = "get-translated-line-items"
const step = createStep(
getTranslatedLineItemsStepId,
async (data: GetTranslatedLineItemsStepInput<any>, { container }) => {
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
if (!isTranslationEnabled || !data.locale || !data.items?.length) {
return new StepResponse(data.items ?? [])
}
await applyTranslations({
localeCode: data.locale,
objects: data.variants,
container,
})
const translatedItems = applyTranslationsToItems(data.items, data.variants)
return new StepResponse(translatedItems)
}
)
/**
* This step translates cart line items based on their associated variant and product IDs.
* It fetches translations for the product (title, description, subtitle) and variant (title),
* then applies them to the corresponding line item fields.
*/
export const getTranslatedLineItemsStep = <T>(
data: GetTranslatedLineItemsStepInput<T>
): ReturnType<StepFunction<any, T[]>> => step(data)

View File

@@ -10,6 +10,8 @@ export * from "./find-or-create-customer"
export * from "./find-sales-channel"
export * from "./get-actions-to-compute-from-promotions"
export * from "./get-line-item-actions"
export * from "./get-translated-line-items"
export * from "./update-cart-items-translations"
export * from "./get-promotion-codes-to-apply"
export * from "./get-variant-price-sets"
export * from "./get-variants"

View File

@@ -0,0 +1,199 @@
import { MedusaContainer } from "@medusajs/framework"
import {
ICartModuleService,
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 "../utils/apply-translations-to-items"
import { productVariantsFields } from "../utils/fields"
export interface UpdateCartItemsTranslationsStepInput {
cart_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 updateCartItemsTranslationsStepId =
"update-cart-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,
{ container }: { container: MedusaContainer }
) {
if (!originalItems?.length) {
return
}
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
for (let i = 0; i < originalItems.length; i += BATCH_SIZE) {
const batch = originalItems.slice(i, i + BATCH_SIZE)
await cartModule.updateLineItems(batch)
}
}
/**
* This step re-translates all cart line items when the cart's locale changes.
* It fetches items and their variants in batches to handle large carts gracefully.
*/
export const updateCartItemsTranslationsStep = createStep(
updateCartItemsTranslationsStepId,
async (data: UpdateCartItemsTranslationsStepInput, { container }) => {
const originalItems: ItemTranslationSnapshot[] = []
try {
const isTranslationEnabled = FeatureFlag.isFeatureEnabled("translation")
if (!isTranslationEnabled || !data.locale) {
return new StepResponse(void 0, [])
}
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
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) => ({
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,
}))
if (itemsToUpdate.length > 0) {
await cartModule.updateLineItems(itemsToUpdate)
}
}
if (data.items?.length) {
await processBatch(data.items)
return new StepResponse(void 0, originalItems)
}
let offset = 0
let hasMore = true
while (hasMore) {
const { data: items } = await query.graph({
entity: "line_items",
filters: { cart_id: data.cart_id },
fields: lineItemFields,
pagination: {
take: BATCH_SIZE,
skip: offset,
},
})
if (items.length === 0) {
hasMore = false
break
}
await processBatch(items as { id: string; variant_id?: string }[])
offset += items.length
hasMore = items.length === BATCH_SIZE
}
return new StepResponse(void 0, originalItems)
} catch (error) {
await compensation(originalItems, { container })
throw error
}
},
compensation
)

View File

@@ -0,0 +1,69 @@
import { ProductVariantDTO } from "@medusajs/framework/types"
const VARIANT_PREFIX = "variant_"
const PRODUCT_PREFIX = "product_"
const PRODUCT_TYPE_PREFIX = "type_"
const PRODUCT_COLLECTION_PREFIX = "collection_"
const TRANSLATABLE_ITEM_PROP_PREFIXES = [
VARIANT_PREFIX,
PRODUCT_PREFIX,
PRODUCT_TYPE_PREFIX,
PRODUCT_COLLECTION_PREFIX,
]
const entityGetterPerPrefix = {
[VARIANT_PREFIX]: (variant: ProductVariantDTO) => variant,
[PRODUCT_PREFIX]: (variant: ProductVariantDTO) => variant.product!,
[PRODUCT_TYPE_PREFIX]: (variant: ProductVariantDTO) => variant.product?.type!,
[PRODUCT_COLLECTION_PREFIX]: (variant: ProductVariantDTO) =>
variant.product?.collection!,
}
function applyTranslation(
itemAny: Record<string, any>,
translatedInput: Record<string, any>,
key: string,
translationKey: string
) {
if (typeof itemAny[key] === typeof translatedInput?.[translationKey]) {
itemAny[key] = translatedInput?.[translationKey]
}
}
/**
* Applies translated variant/product fields to line items.
*/
export function applyTranslationsToItems<
T extends { variant_id?: string; [key: string]: any }
>(items: T[], variants: Partial<ProductVariantDTO>[]): T[] {
const variantMap = new Map(variants.map((variant) => [variant.id, variant]))
return items.map((item) => {
if (!item.variant_id) {
return item
}
const variant = variantMap.get(item.variant_id)
if (!variant) {
return item
}
const itemAny = item as Record<string, any>
Object.entries(itemAny).forEach(([key, value]) => {
for (const prefix of TRANSLATABLE_ITEM_PROP_PREFIXES) {
if (key.startsWith(prefix)) {
const translationKey = key.replace(prefix, "")
const entity = entityGetterPerPrefix[prefix](variant)
if (!entity) {
break
}
applyTranslation(itemAny, entity, key, translationKey)
}
}
})
return item
})
}

View File

@@ -7,6 +7,7 @@ export const cartFieldsForRefreshSteps = [
"quantity",
"subtotal",
"item_total",
"locale",
"total",
"item_subtotal",
"shipping_subtotal",

View File

@@ -139,30 +139,30 @@ export function prepareLineItemData(data: PrepareLineItemDataInput) {
let lineItem: any = {
quantity: item?.quantity,
title: variant?.product?.title ?? item?.title,
subtitle: variant?.title ?? item?.subtitle,
title: item?.title ?? variant?.product?.title,
subtitle: item?.subtitle ?? variant?.title,
thumbnail:
variant?.thumbnail ?? variant?.product?.thumbnail ?? item?.thumbnail,
item?.thumbnail ?? variant?.thumbnail ?? variant?.product?.thumbnail,
product_id: variant?.product?.id ?? item?.product_id,
product_title: variant?.product?.title ?? item?.product_title,
product_title: item?.product_title ?? variant?.product?.title,
product_description:
variant?.product?.description ?? item?.product_description,
product_subtitle: variant?.product?.subtitle ?? item?.product_subtitle,
product_type: variant?.product?.type?.value ?? item?.product_type ?? null,
item?.product_description ?? variant?.product?.description,
product_subtitle: item?.product_subtitle ?? variant?.product?.subtitle,
product_type: item?.product_type ?? variant?.product?.type?.value ?? null,
product_type_id:
variant?.product?.type?.id ?? item?.product_type_id ?? null,
item?.product_type_id ?? variant?.product?.type?.id ?? null,
product_collection:
variant?.product?.collection?.title ?? item?.product_collection ?? null,
product_handle: variant?.product?.handle ?? item?.product_handle,
item?.product_collection ?? variant?.product?.collection?.title ?? null,
product_handle: item?.product_handle ?? variant?.product?.handle,
variant_id: variant?.id,
variant_sku: variant?.sku ?? item?.variant_sku,
variant_barcode: variant?.barcode ?? item?.variant_barcode,
variant_title: variant?.title ?? item?.variant_title,
variant_sku: item?.variant_sku ?? variant?.sku,
variant_barcode: item?.variant_barcode ?? variant?.barcode,
variant_title: item?.variant_title ?? variant?.title,
variant_option_values: item?.variant_option_values,
is_discountable: variant?.product?.discountable ?? item?.is_discountable,
is_discountable: item?.is_discountable ?? variant?.product?.discountable,
is_giftcard: variant?.product?.is_giftcard ?? false,
requires_shipping: requiresShipping,

View File

@@ -23,6 +23,7 @@ import { acquireLockStep, releaseLockStep } from "../../locking"
import {
createLineItemsStep,
getLineItemActionsStep,
getTranslatedLineItemsStep,
updateLineItemsStep,
} from "../steps"
import { validateCartStep } from "../steps/validate-cart"
@@ -42,7 +43,9 @@ import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
import { getVariantsAndItemsWithPrices } from "./get-variants-and-items-with-prices"
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
const cartFields = ["completed_at"].concat(cartFieldsForPricingContext)
const cartFields = ["completed_at", "locale"].concat(
cartFieldsForPricingContext
)
export const addToCartWorkflowId = "add-to-cart"
/**
@@ -292,10 +295,33 @@ export const addToCartWorkflow = createWorkflow(
},
})
const itemsToCreateVariants = transform(
{ itemsToCreate, variants } as {
itemsToCreate: CreateLineItemForCartDTO[]
variants: PrepareVariantLineItemInput[]
},
(data) => {
if (!data.itemsToCreate?.length) {
return []
}
const variantsMap = new Map(data.variants?.map((v) => [v.id, v]))
return data.itemsToCreate
.map((item) => item.variant_id && variantsMap.get(item.variant_id))
.filter(Boolean) as PrepareVariantLineItemInput[]
}
)
const translatedItemsToCreate = getTranslatedLineItemsStep({
items: itemsToCreate,
variants: itemsToCreateVariants,
locale: cart.locale,
})
const [createdLineItems, updatedLineItems] = parallelize(
createLineItemsStep({
id: cart.id,
items: itemsToCreate,
items: translatedItemsToCreate,
}),
updateLineItemsStep({
id: cart.id,

View File

@@ -3,6 +3,7 @@ import {
ConfirmVariantInventoryWorkflowInputDTO,
CreateCartDTO,
CreateCartWorkflowInputDTO,
CreateLineItemDTO,
} from "@medusajs/framework/types"
import {
CartWorkflowEvents,
@@ -23,6 +24,7 @@ import {
findOneOrAnyRegionStep,
findOrCreateCustomerStep,
findSalesChannelStep,
getTranslatedLineItemsStep,
} from "../steps"
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
import { productVariantsFields } from "../utils/fields"
@@ -205,17 +207,31 @@ export const createCartWorkflow = createWorkflow(
}
}
return data_
return data_ as CreateCartDTO
}
)
const cartToCreate = transform({ lineItems, cartInput }, (data) => {
return {
...data.cartInput,
items: data.lineItems.map((i) => i.data),
} as unknown as CreateCartDTO
const itemsToCreate = transform({ lineItems }, (data) => {
return data.lineItems.map((i) => i.data as CreateLineItemDTO)
})
const translatedItems = getTranslatedLineItemsStep({
items: itemsToCreate,
variants,
locale: input.locale,
})
const cartToCreate = transform(
{ cartInput, translatedItems } as unknown as {
cartInput: CreateCartDTO
translatedItems: CreateLineItemDTO[]
},
(data) => {
data.cartInput.items = data.translatedItems
return data.cartInput as unknown as CreateCartDTO
}
)
const validate = createHook("validate", {
input: cartInput,
cart: cartToCreate,

View File

@@ -5,6 +5,7 @@ import {
CreateCartCreateLineItemDTO,
CustomerDTO,
OrderWorkflow,
ProductVariantDTO,
RegionDTO,
UpdateLineItemDTO,
UpdateLineItemWithSelectorDTO,
@@ -54,7 +55,7 @@ interface GetVariantsAndItemsWithPricesWorkflowInput {
type GetVariantsAndItemsWithPricesWorkflowOutput = {
// The variant can depend on the requested fields and therefore the caller will know better
variants: (object & {
variants: (Partial<ProductVariantDTO> & {
calculated_price: {
calculated_price: {
price_list_type: string
@@ -184,8 +185,11 @@ export const getVariantsAndItemsWithPrices = createWorkflow(
}
const variant = variantsData.find((v) => v.id === item.variant_id)
if ((item.variant_id && !variant) || // variant specified but doesn't exist
(variant && (!variant?.product?.status || variant.product.status !== ProductStatus.PUBLISHED)) // variant exists but product is not published
if (
(item.variant_id && !variant) || // variant specified but doesn't exist
(variant &&
(!variant?.product?.status ||
variant.product.status !== ProductStatus.PUBLISHED)) // variant exists but product is not published
) {
variantNotFoundOrPublished.push(item_.variant_id)
}
@@ -225,7 +229,9 @@ export const getVariantsAndItemsWithPrices = createWorkflow(
if (variantNotFoundOrPublished.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Variants ${variantNotFoundOrPublished.join(", ")} do not exist or belong to a product that is not published`
`Variants ${variantNotFoundOrPublished.join(
", "
)} do not exist or belong to a product that is not published`
)
}
if (priceNotFound.length > 0) {

View File

@@ -10,7 +10,11 @@ import {
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "../../common"
import { acquireLockStep, releaseLockStep } from "../../locking"
import { updateLineItemsStep, validateCartStep } from "../steps"
import {
updateCartItemsTranslationsStep,
updateLineItemsStep,
validateCartStep,
} from "../steps"
import { cartFieldsForRefreshSteps } from "../utils/fields"
import { pricingContextResult } from "../utils/schemas"
import { getVariantsAndItemsWithPrices } from "./get-variants-and-items-with-prices"
@@ -54,6 +58,12 @@ export type RefreshCartItemsWorkflowInput = {
* on the configurations of the cart's tax region.
*/
force_tax_calculation?: boolean
/**
* The new locale code to update cart items translations.
* When provided, all cart items will be re-translated using this locale.
*/
locale?: string
}
export const refreshCartItemsWorkflowId = "refresh-cart-items"
@@ -234,6 +244,16 @@ export const refreshCartItemsWorkflow = createWorkflow(
},
})
when("should-update-item-translations", { input }, ({ input }) => {
return !!input.locale
}).then(() => {
updateCartItemsTranslationsStep({
cart_id: input.cart_id,
locale: input.locale!,
items: refetchedCart.items,
})
})
const beforeRefreshingPaymentCollection = createHook(
"beforeRefreshingPaymentCollection",
{ input }

View File

@@ -99,6 +99,7 @@ export const updateCartWorkflow = createWorkflow(
"email",
"customer_id",
"sales_channel_id",
"locale",
"shipping_address.*",
"region.*",
"region.countries.*",
@@ -280,6 +281,17 @@ export const updateCartWorkflow = createWorkflow(
}).config({ name: "emit-region-updated" })
})
// Get the new locale code if it's being updated
const newLocaleCode = transform(
{ input, cartToUpdate },
({ input, cartToUpdate }) => {
if (isDefined(input.locale) && input.locale !== cartToUpdate?.locale) {
return input.locale
}
return undefined
}
)
parallelize(
updateCartsStep([cartInput]),
emitEventStep({
@@ -314,6 +326,7 @@ export const updateCartWorkflow = createWorkflow(
cart_id: cartInput.id,
promo_codes: input.promo_codes,
force_refresh: !!newRegion,
locale: newLocaleCode,
additional_data: input.additional_data,
},
})

View File

@@ -14,7 +14,7 @@ export const createTranslationsStepId = "create-translations"
* {
* reference_id: "prod_123",
* reference: "product",
* locale_code: "fr-FR",
* locale: "fr-FR",
* translations: { title: "Produit", description: "Description du produit" }
* }
* ])

View File

@@ -36,7 +36,7 @@ export const updateTranslationsStepId = "update-translations"
* const data = updateTranslationsStep({
* selector: {
* reference_id: "prod_123",
* locale_code: "fr-FR"
* locale: "fr-FR"
* },
* update: {
* translations: { title: "Nouveau titre" }

View File

@@ -24,7 +24,7 @@ export const validateTranslationsStep = createStep(
} = await query.graph(
{
entity: "store",
fields: ["supported_locales.*"],
fields: ["id", "supported_locales.*"],
pagination: {
take: 1,
},

View File

@@ -28,7 +28,7 @@ export const createTranslationsWorkflowId = "create-translations"
* {
* reference_id: "prod_123",
* reference: "product",
* locale_code: "fr-FR",
* locale: "fr-FR",
* translations: { title: "Produit", description: "Description du produit" }
* }
* ]

View File

@@ -24,7 +24,7 @@ export const updateTranslationsWorkflowId = "update-translations"
* input: {
* selector: {
* reference_id: "prod_123",
* locale_code: "fr-FR"
* locale: "fr-FR"
* },
* update: {
* translations: { title: "Nouveau titre" }

View File

@@ -769,6 +769,11 @@ export interface CartDTO {
*/
currency_code: string
/**
* The locale code of the cart.
*/
locale?: string
/**
* The associated shipping address.
*

View File

@@ -116,6 +116,11 @@ export interface CreateCartDTO {
*/
currency_code: string
/**
* The locale code of the cart.
*/
locale?: string
/**
* The associated shipping address's ID.
*/
@@ -176,6 +181,11 @@ export interface UpdateCartDataDTO {
*/
currency_code?: string
/**
* The locale code of the cart.
*/
locale?: string | null
/**
* The associated shipping address's ID.
*/

View File

@@ -279,6 +279,11 @@ export interface CreateCartWorkflowInputDTO {
* The promotional codes applied on the cart.
*/
promo_codes?: string[]
/**
* The locale code of the cart.
*/
locale?: string
}
/**
@@ -336,6 +341,11 @@ export interface UpdateCartWorkflowInputDTO {
*/
currency_code?: string
/**
* The locale code for the cart.
*/
locale?: string | null
/**
* Custom key-value pairs of data related to the cart.
*/
@@ -553,4 +563,4 @@ export type CreateCartCreditLinesWorkflowInput = {
* The metadata of the cart detail
*/
metadata: Record<string, unknown> | null
}[]
}[]

View File

@@ -39,6 +39,11 @@ export interface StoreCreateCart {
* Key-value pairs of custom data.
*/
metadata?: Record<string, unknown>
/**
* The locale code of the cart.
*/
locale?: string
}
export interface StoreUpdateCart {
@@ -71,6 +76,10 @@ export interface StoreUpdateCart {
* The promotion codes to apply on the cart.
*/
promo_codes?: string[]
/**
* The locale code of the cart.
*/
locale?: string
}
export interface StoreUpdateCartCustomer {}
@@ -187,4 +196,4 @@ export interface StoreCartAddPromotion {
* The promotion codes to add to the cart.
*/
promo_codes: string[]
}
}

View File

@@ -288,6 +288,7 @@ class MedusaTestRunner {
cwd: this.cwd,
})
await medusaAppLoader.runModulesLoader()
await createDefaultsWorkflow(copiedContainer).run()
} catch (error) {
await copiedContainer.dispose?.()

View File

@@ -6,12 +6,12 @@ import {
FeatureFlag,
} from "@medusajs/framework/utils"
import { BatchMethodRequest, HttpTypes } from "@medusajs/types"
import TranslationFeatureFlag from "../../../../feature-flags/translation"
import { defaultAdminTranslationFields } from "../query-config"
import {
AdminCreateTranslationType,
AdminUpdateTranslationType,
} from "../validators"
import TranslationFeatureFlag from "../../../../feature-flags/translation"
export const POST = async (
req: AuthenticatedMedusaRequest<

View File

@@ -23,6 +23,7 @@ export const CreateCart = z
sales_channel_id: z.string().nullish(),
promo_codes: z.array(z.string()).optional(),
metadata: z.record(z.unknown()).nullish(),
locale: z.string().optional(),
})
.strict()
export const StoreCreateCart = WithAdditionalData(CreateCart)
@@ -57,6 +58,7 @@ export const UpdateCart = z
sales_channel_id: z.string().nullish(),
metadata: z.record(z.unknown()).nullish(),
promo_codes: z.array(z.string()).optional(),
locale: z.string().optional(),
})
.strict()
export const StoreUpdateCart = WithAdditionalData(UpdateCart)

View File

@@ -166,7 +166,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_address_deleted_at\" ON \"cart_address\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_address_deleted_at\" ON \"cart_address\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "cart_address_pkey",
@@ -239,6 +239,15 @@
"nullable": false,
"mappedType": "text"
},
"locale": {
"name": "locale",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -319,7 +328,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_deleted_at\" ON \"cart\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_deleted_at\" ON \"cart\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_region_id",
@@ -328,7 +337,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_region_id\" ON \"cart\" (region_id) WHERE deleted_at IS NULL AND region_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_region_id\" ON \"cart\" (\"region_id\") WHERE deleted_at IS NULL AND region_id IS NOT NULL"
},
{
"keyName": "IDX_cart_customer_id",
@@ -337,7 +346,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_customer_id\" ON \"cart\" (customer_id) WHERE deleted_at IS NULL AND customer_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_customer_id\" ON \"cart\" (\"customer_id\") WHERE deleted_at IS NULL AND customer_id IS NOT NULL"
},
{
"keyName": "IDX_cart_sales_channel_id",
@@ -346,7 +355,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_sales_channel_id\" ON \"cart\" (sales_channel_id) WHERE deleted_at IS NULL AND sales_channel_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_sales_channel_id\" ON \"cart\" (\"sales_channel_id\") WHERE deleted_at IS NULL AND sales_channel_id IS NOT NULL"
},
{
"keyName": "IDX_cart_curency_code",
@@ -355,7 +364,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_curency_code\" ON \"cart\" (currency_code) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_curency_code\" ON \"cart\" (\"currency_code\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_shipping_address_id",
@@ -364,7 +373,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_address_id\" ON \"cart\" (shipping_address_id) WHERE deleted_at IS NULL AND shipping_address_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_address_id\" ON \"cart\" (\"shipping_address_id\") WHERE deleted_at IS NULL AND shipping_address_id IS NOT NULL"
},
{
"keyName": "IDX_cart_billing_address_id",
@@ -373,7 +382,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_billing_address_id\" ON \"cart\" (billing_address_id) WHERE deleted_at IS NULL AND billing_address_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_billing_address_id\" ON \"cart\" (\"billing_address_id\") WHERE deleted_at IS NULL AND billing_address_id IS NOT NULL"
},
{
"keyName": "cart_pkey",
@@ -525,7 +534,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_credit_line_cart_id\" ON \"credit_line\" (cart_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_credit_line_cart_id\" ON \"credit_line\" (\"cart_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_credit_line_deleted_at",
@@ -534,7 +543,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_credit_line_deleted_at\" ON \"credit_line\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_credit_line_deleted_at\" ON \"credit_line\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_credit_line_reference_reference_id",
@@ -543,7 +552,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_credit_line_reference_reference_id\" ON \"credit_line\" (reference, reference_id) WHERE deleted_at IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_credit_line_reference_reference_id\" ON \"credit_line\" (\"reference\", \"reference_id\") WHERE deleted_at IS NOT NULL"
},
{
"keyName": "credit_line_pkey",
@@ -884,7 +893,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_cart_id\" ON \"cart_line_item\" (cart_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_cart_id\" ON \"cart_line_item\" (\"cart_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_line_item_deleted_at",
@@ -893,7 +902,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_deleted_at\" ON \"cart_line_item\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_deleted_at\" ON \"cart_line_item\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_line_item_variant_id",
@@ -902,7 +911,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_variant_id\" ON \"cart_line_item\" (variant_id) WHERE deleted_at IS NULL AND variant_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_variant_id\" ON \"cart_line_item\" (\"variant_id\") WHERE deleted_at IS NULL AND variant_id IS NOT NULL"
},
{
"keyName": "IDX_line_item_product_id",
@@ -911,7 +920,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_product_id\" ON \"cart_line_item\" (product_id) WHERE deleted_at IS NULL AND product_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_product_id\" ON \"cart_line_item\" (\"product_id\") WHERE deleted_at IS NULL AND product_id IS NOT NULL"
},
{
"keyName": "IDX_line_item_product_type_id",
@@ -920,7 +929,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_product_type_id\" ON \"cart_line_item\" (product_type_id) WHERE deleted_at IS NULL AND product_type_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_product_type_id\" ON \"cart_line_item\" (\"product_type_id\") WHERE deleted_at IS NULL AND product_type_id IS NOT NULL"
},
{
"keyName": "cart_line_item_pkey",
@@ -1087,7 +1096,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_adjustment_item_id\" ON \"cart_line_item_adjustment\" (item_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_adjustment_item_id\" ON \"cart_line_item_adjustment\" (\"item_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_line_item_adjustment_deleted_at",
@@ -1096,7 +1105,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_adjustment_deleted_at\" ON \"cart_line_item_adjustment\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_adjustment_deleted_at\" ON \"cart_line_item_adjustment\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_line_item_adjustment_promotion_id",
@@ -1105,7 +1114,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_adjustment_promotion_id\" ON \"cart_line_item_adjustment\" (promotion_id) WHERE deleted_at IS NULL AND promotion_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_adjustment_promotion_id\" ON \"cart_line_item_adjustment\" (\"promotion_id\") WHERE deleted_at IS NULL AND promotion_id IS NOT NULL"
},
{
"keyName": "cart_line_item_adjustment_pkey",
@@ -1259,7 +1268,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_tax_line_item_id\" ON \"cart_line_item_tax_line\" (item_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_tax_line_item_id\" ON \"cart_line_item_tax_line\" (\"item_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_line_item_tax_line_deleted_at",
@@ -1268,7 +1277,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_tax_line_deleted_at\" ON \"cart_line_item_tax_line\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_line_item_tax_line_deleted_at\" ON \"cart_line_item_tax_line\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_line_item_tax_line_tax_rate_id",
@@ -1277,7 +1286,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_tax_line_tax_rate_id\" ON \"cart_line_item_tax_line\" (tax_rate_id) WHERE deleted_at IS NULL AND tax_rate_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_line_item_tax_line_tax_rate_id\" ON \"cart_line_item_tax_line\" (\"tax_rate_id\") WHERE deleted_at IS NULL AND tax_rate_id IS NOT NULL"
},
{
"keyName": "cart_line_item_tax_line_pkey",
@@ -1444,7 +1453,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_cart_id\" ON \"cart_shipping_method\" (cart_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_cart_id\" ON \"cart_shipping_method\" (\"cart_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_shipping_method_deleted_at",
@@ -1453,7 +1462,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_deleted_at\" ON \"cart_shipping_method\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_deleted_at\" ON \"cart_shipping_method\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_shipping_method_option_id",
@@ -1462,7 +1471,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_option_id\" ON \"cart_shipping_method\" (shipping_option_id) WHERE deleted_at IS NULL AND shipping_option_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_option_id\" ON \"cart_shipping_method\" (\"shipping_option_id\") WHERE deleted_at IS NULL AND shipping_option_id IS NOT NULL"
},
{
"keyName": "cart_shipping_method_pkey",
@@ -1625,7 +1634,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_adjustment_shipping_method_id\" ON \"cart_shipping_method_adjustment\" (shipping_method_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_adjustment_shipping_method_id\" ON \"cart_shipping_method_adjustment\" (\"shipping_method_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_shipping_method_adjustment_deleted_at",
@@ -1634,7 +1643,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_adjustment_deleted_at\" ON \"cart_shipping_method_adjustment\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_adjustment_deleted_at\" ON \"cart_shipping_method_adjustment\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_shipping_method_adjustment_promotion_id",
@@ -1643,7 +1652,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_adjustment_promotion_id\" ON \"cart_shipping_method_adjustment\" (promotion_id) WHERE deleted_at IS NULL AND promotion_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_adjustment_promotion_id\" ON \"cart_shipping_method_adjustment\" (\"promotion_id\") WHERE deleted_at IS NULL AND promotion_id IS NOT NULL"
},
{
"keyName": "cart_shipping_method_adjustment_pkey",
@@ -1790,7 +1799,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_tax_line_shipping_method_id\" ON \"cart_shipping_method_tax_line\" (shipping_method_id) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_tax_line_shipping_method_id\" ON \"cart_shipping_method_tax_line\" (\"shipping_method_id\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_cart_shipping_method_tax_line_deleted_at",
@@ -1799,7 +1808,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_tax_line_deleted_at\" ON \"cart_shipping_method_tax_line\" (deleted_at) WHERE deleted_at IS NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_cart_shipping_method_tax_line_deleted_at\" ON \"cart_shipping_method_tax_line\" (\"deleted_at\") WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_shipping_method_tax_line_tax_rate_id",
@@ -1808,7 +1817,7 @@
"constraint": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_tax_line_tax_rate_id\" ON \"cart_shipping_method_tax_line\" (tax_rate_id) WHERE deleted_at IS NULL AND tax_rate_id IS NOT NULL"
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_method_tax_line_tax_rate_id\" ON \"cart_shipping_method_tax_line\" (\"tax_rate_id\") WHERE deleted_at IS NULL AND tax_rate_id IS NOT NULL"
},
{
"keyName": "cart_shipping_method_tax_line_pkey",

View File

@@ -0,0 +1,13 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20251208130704 extends Migration {
override async up(): Promise<void> {
this.addSql(
`alter table if exists "cart" add column if not exists "locale" text null;`
)
}
override async down(): Promise<void> {
this.addSql(`alter table if exists "cart" drop column if exists "locale";`)
}
}

View File

@@ -12,6 +12,7 @@ const Cart = model
sales_channel_id: model.text().nullable(),
email: model.text().nullable(),
currency_code: model.text(),
locale: model.text().nullable(),
metadata: model.json().nullable(),
completed_at: model.dateTime().nullable(),
shipping_address: model

2879
yarn.lock

File diff suppressed because it is too large Load Diff