Skip to content

[Bug]: Product updates cannot atomically add option values used by replacement variants #16304

Description

@ryanngit

Product updates cannot atomically add option values used by replacement variants

Package.json file

{
  "name": "root",
  "private": true,
  "workspaces": {
    "packages": [
      "packages/medusa",
      "packages/medusa-test-utils",
      "packages/modules/*",
      "packages/modules/providers/*",
      "packages/plugins/*",
      "packages/core/*",
      "packages/framework/*",
      "packages/cli/*",
      "packages/cli/oas/*",
      "packages/*",
      "packages/admin/*",
      "packages/design-system/*",
      "packages/generated/*",
      "integration-tests/**/*"
    ]
  },
  "devDependencies": {
    "@atomico/rollup-plugin-sizes": "^1.1.4",
    "@faker-js/faker": "^9.2.0",
    "@medusajs/eslint-plugin": "workspace:^",
    "@rollup/plugin-node-resolve": "^15.1.0",
    "@rollup/plugin-replace": "^5.0.2",
    "@storybook/addon-essentials": "^8.3.5",
    "@storybook/addon-interactions": "^8.3.5",
    "@storybook/addon-links": "^8.3.5",
    "@storybook/addon-styling": "^1.3.7",
    "@storybook/blocks": "^8.3.5",
    "@storybook/react": "^8.3.5",
    "@storybook/react-vite": "^8.3.5",
    "@storybook/testing-library": "^0.2.2",
    "@swc/core": "^1.7.28",
    "@swc/helpers": "^0.5.11",
    "@swc/jest": "^0.2.36",
    "@testing-library/dom": "^9.3.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "^14.4.3",
    "@types/autocannon": "^7.12.5",
    "@types/chalk": "^2.2.0",
    "@types/commander": "^2.12.2",
    "@types/compression": "^1.7.5",
    "@types/configstore": "^6.0.0",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/fs-extra": "11.0.1",
    "@types/inquirer": "^9.0.3",
    "@types/js-yaml": "^4.0.9",
    "@types/jsdom": "^27.0.0",
    "@types/jsonwebtoken": "^8.5.9",
    "@types/lodash": "^4.17.15",
    "@types/lodash.debounce": "^4.0.9",
    "@types/lodash.isequal": "^4.5.8",
    "@types/lodash.mergewith": "^4.6.9",
    "@types/multer": "^2.0.0",
    "@types/node": "^20.12.11",
    "@types/node-emoji": "^1.8.2",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "@types/validator": "^13.7.17",
    "@types/wait-on": "^5.3.1",
    "@types/yargs": "^15.0.15",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "@typescript-eslint/rule-tester": "^8.0.0",
    "@vitejs/plugin-react": "^4.2.1",
    "@vitest/coverage-v8": "^0.32.2",
    "ajv": "^8.18.0",
    "autocannon": "^8.0.0",
    "autoprefixer": "^10.4.19",
    "awilix": "^8.0.1",
    "axios": "^1.13.1",
    "axios-mock-adapter": "^1.19.0",
    "chromatic": "^6.20.0",
    "configstore": "^5.0.1",
    "cross-env": "^5.2.1",
    "esbuild": "^0.25.0",
    "eslint": "^8.40.0",
    "eslint-config-google": "^0.14.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-react": "^7.31.11",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "eslint-plugin-storybook": "^10.0.1",
    "eslint-plugin-unused-imports": "^2.0.0",
    "expect-type": "^0.20.0",
    "express": "^4.21.0",
    "faker": "^5.5.3",
    "get-port": "^5.1.1",
    "glob": "13.0.6",
    "import-from": "^3.0.0",
    "ioredis": "^5.4.1",
    "jest": "^29.7.0",
    "js-yaml": "^4.2.0",
    "jsdom": "^27.1.0",
    "lint-staged": "^11.2.6",
    "msw": "^2.3.0",
    "node-notifier": "^10.0.1",
    "nodemon": "^2.0.20",
    "pg-god": "^1.0.12",
    "postcss": "^8.5.10",
    "posthog-node": "^5.11.0",
    "prettier": "^2.8.8",
    "prettier-plugin-tailwindcss": "^0.2.3",
    "prop-types": "^15.8.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-router-dom": "6.30.3",
    "resize-observer-polyfill": "^1.5.1",
    "resolve-cwd": "^3.0.0",
    "rimraf": "^5.0.1",
    "rollup": "^4.28.1",
    "rollup-plugin-esbuild": "^5.0.0",
    "rollup-plugin-ts": "^3.2.0",
    "rollup-plugin-visualizer": "^5.9.2",
    "storybook": "^8.3.5",
    "supertest": "^7.1.4",
    "tailwindcss": "^3.4.3",
    "ts-jest": "^29.1.0",
    "ts-node": "^10.9.2",
    "tsc-alias": "^1.8.7",
    "tsup": "^8.4.0",
    "turbo": "^1.6.3",
    "typescript": "^5.6.2",
    "uuid": "^9.0.0",
    "vite": "^5.4.21",
    "vite-plugin-inspect": "^0.8.7",
    "vite-plugin-turbosnap": "^1.0.2",
    "vitest": "^3.0.5",
    "yalc": "^1.0.0-pre.53",
    "zod": "4.2.0",
    "zod-validation-error": "5.0.0"
  },
  "scripts": {
    "build": "turbo run build build:plugin --concurrency=100% --no-daemon",
    "lint": "eslint --ignore-path .eslintignore --ext .js,.ts,.tsx .",
    "lint:medusa": "eslint --no-eslintrc --config eslint.medusa.cjs --ignore-path .eslintignore --ext .js,.ts,.tsx .",
    "lint:path": "eslint --ignore-path .eslintignore --ext .js,.ts,.tsx",
    "prettier": "prettier",
    "jest": "jest",
    "test": "turbo run test --no-daemon --no-cache --force",
    "test:chunk": "./scripts/run-workspace-unit-tests-in-chunks.sh",
    "test:integration:packages:fast": "turbo run test:integration --concurrency=1 --no-daemon --no-cache --force --filter='./packages/core/*' --filter='./packages/medusa' --filter='./packages/modules/*' --filter='./packages/modules/providers/*' --filter='!./packages/modules/{workflow-engine-redis,index,product,order,cart}'",
    "test:integration:packages:slow": "turbo run test:integration --concurrency=1 --no-daemon --no-cache --force --filter='./packages/modules/{workflow-engine-redis,index,product,order,cart}'",
    "test:integration:packages": "turbo run test:integration --concurrency=1 --no-daemon --no-cache --force --filter='./packages/core/*' --filter='./packages/medusa' --filter='./packages/modules/*' --filter='./packages/modules/providers/*'",
    "test:integration:http": "turbo run test:integration:chunk --no-daemon --no-cache --force --filter=integration-tests-http",
    "test:integration:modules": "turbo run test:integration:chunk --no-daemon --no-cache --force --filter=integration-tests-modules",
    "generate:http-types": "yarn workspace @medusajs/http-types-generator generate:http-types",
    "validate:http-types": "yarn workspace @medusajs/http-types-generator validate:http-types",
    "openapi:generate": "yarn ./packages/cli/oas/oas-github-ci run ci --with-full-file",
    "medusa-oas": "yarn ./packages/cli/oas/medusa-oas-cli run medusa-oas",
    "release:snapshot": "changeset publish --no-git-tags --snapshot --tag snapshot",
    "release:next": "chgstangeset publish --no-git-tags --snapshot --tag next",
    "version:next": "changeset version --snapshot next",
    "release": "changeset publish",
    "version": "changeset version && yarn install --no-immutable"
  },
  "dependencies": {
    "@changesets/changelog-github": "^0.4.8",
    "@changesets/cli": "^2.26.0",
    "import-from": "^3.0.0"
  },
  "resolutions": {
    "@redocly/cli/react": "^17.0.1",
    "@redocly/cli/react-dom": "^17.0.1",
    "lodash": "^4.18.1",
    "pg": "8.16.3",
    "@changesets/get-github-info@^0.5.2": "patch:@changesets/get-github-info@npm:^0.5.2#.yarn/patches/changesets-github-info.patch",
    "@types/react": "^18.3.2",
    "@types/react-dom": "^18.2.25",
    "semver": "^7.5.2",
    "axios": "^1.13.1",
    "esbuild": "^0.25.0",
    "on-headers": "^1.1.0",
    "postcss": "^8.5.10",
    "validator": "^13.15.20",
    "@opentelemetry/core": "^2.7.0",
    "@rushstack/node-core-library/ajv": "^8.18.0",
    "@changesets/assemble-release-plan@^6.0.9": "patch:@changesets/assemble-release-plan@npm:^6.0.9#.yarn/patches/@changesets-assemble-release-plan-npm-6.0.9-e01af97ef4.patch"
  },
  "packageManager": "yarn@3.2.1"
}

Node.js version

v22.23.1

Database and its version

PostgreSQL 15.4

Operating system name and version

Windows 10.0.19045

Browser name

Not applicable.

What happened?

A product variant update can only resolve option values already linked to the
product. Adding a new option value and replacing a variant with one that uses
that value therefore requires two Product Module calls.

When those calls run without a shared outer transaction, the option-value
update commits before the variant update begins. If the variant update fails,
the new value link remains. Another writer can also observe or change the
partial state between calls.

Expected behavior

One product update should be able to add or remove product option values and
replace variants in the same transaction. A failure should roll back both the
value-link changes and variant changes.

Actual behavior

The supported product update contract has no atomic option-value delta. A
variant cannot reference a newly added value until a separate value-link
operation commits.

Reproduction

Source: https://github.com/medusajs/medusa/tree/e037c71ff11add91583124a600840ce793421fe7

Inside the existing updateProducts integration block in
packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts,
add:

it("updates a new option value and its variant atomically", async () => {
  const option = productOne.options[0];
  const variant = productOne.variants[0];

  await service.updateProducts(productOne.id, {
    option_value_updates: [
      {
        product_option_id: option.id,
        add: [{ value: "val-3" }],
      },
    ],
    variants: [
      {
        id: variant.id,
        title: variant.title,
        options: { "opt-title": "val-3" },
      },
    ],
  } as any);

  const product = await service.retrieveProduct(productOne.id, {
    relations: ["options.values", "variants.options"],
  });

  expect(product.options[0].values.map((value) => value.value)).toContain(
    "val-3"
  );
  expect(product.variants[0].options).toEqual([
    expect.objectContaining({ value: "val-3" }),
  ]);
});

Run:

corepack yarn workspace @medusajs/product test:integration --runInBand --testPathPattern='product-module-service/products.spec.ts' --testNamePattern='updates a new option value and its variant atomically'

On the linked develop commit, the update rejects with
Option value val-3 does not exist for option opt-title; the value cannot be
added in the same call that replaces the variant.

Proposed contract

Accept an ID-scoped option_value_updates delta alongside variants, apply
additions before variant resolution and removals afterward, and execute all
changes in one Product Module transaction with guarded workflow compensation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions