Skip to content

fix(core): Assign new variants to all product channels#4699

Open
Ryrahul wants to merge 2 commits into
vendurehq:masterfrom
Ryrahul:fix/channel-product-variant-assingment
Open

fix(core): Assign new variants to all product channels#4699
Ryrahul wants to merge 2 commits into
vendurehq:masterfrom
Ryrahul:fix/channel-product-variant-assingment

Conversation

@Ryrahul
Copy link
Copy Markdown
Contributor

@Ryrahul Ryrahul commented May 5, 2026

Fixes #4532

Description

When a new product variant is created after a channel has already been assigned to the product, the new variant does not appear in that channel. This is because createSingle in ProductVariantService only assigns the variant to the current channel + default channel via assignToCurrentChannel(), without checking what other channels the parent product belongs to.

Root Cause

There is an asymmetry in channel assignment:

  • assignProductsToChannel assigns the product AND all its existing variants, option groups, and options to the target channel.
  • createSingle (variant creation) only assigns the new variant to ctx.channelId + default channel — ignoring any other channels the parent product is already assigned to.

Fix

After creating the variant and its prices for the current/default channel, we now:

  1. Load the parent product's channels (using getRepository().findOne() with relations: ['channels'] and relationLoadStrategy: 'query' — same pattern as assignProductsToChannel).
  2. Filter out channels already handled (current + default).
  3. For each additional channel:
    • Assign the variant via channelService.assignToChannels().
    • Create a ProductVariantPrice via createOrUpdateProductVariantPrice().
    • Assign the variant's option groups and options via channelService.assignToChannels() — matching what assignProductsToChannel does.

All methods used are existing public APIs already called elsewhere in the same file. No new patterns introduced.

Price handling

The new variant's price in additional channels uses input.price directly (priceFactor = 1). The original priceFactor from the initial assignProductsToChannel call is a one-time conversion factor that is not stored anywhere, so it cannot be retroactively applied to new variants. The admin can adjust channel-specific prices after creation. This is strictly better than the current behavior where the variant is completely invisible in the other channel.

Option/OptionGroup handling

When a new option is created after the product was assigned to a channel (e.g. adding a new color), ProductOptionService.create() only assigns it to the current + default channel via assignToCurrentChannel(). The fix also assigns the new variant's options and their option groups to the additional channels, preventing Cannot return null for non-nullable field ProductOption.group errors.

Scope

This fix is specific to ProductVariant creation. The same gap exists for other parent-child relationships:

Parent -> Child Same gap? Impact
Product -> ProductVariant Fixed in this PR High
Facet -> FacetValue Yes New facet values created after facet is assigned to a channel won't appear there
ProductOptionGroup -> ProductOption Yes New options won't appear in assigned channels
Collection -> Child Collection Yes Child collections created after parent is assigned won't appear there

All of these use the same assignToCurrentChannel() pattern on creation without checking the parent's channels.

Question for maintainers: Would you prefer a common helper/pattern that handles this for all parent-child relationships, or is scoping to ProductVariant sufficient for now?

Breaking changes

None. This is purely additive — new variants now get assigned to more channels than before. Existing behavior for variants created in products that are only in the default channel is unchanged (the additional channels list is empty, so no extra work is done).

Checklist

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR
  • I have added or updated test cases
  • I have updated the README if needed

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment May 11, 2026 4:16pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1aa4e52e-b5a2-4d6d-94cc-7fdd0590a1bd

📥 Commits

Reviewing files that changed from the base of the PR and between 329ad04 and ac938da.

📒 Files selected for processing (1)
  • packages/core/src/service/services/product-variant.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/service/services/product-variant.service.ts

📝 Walkthrough

Walkthrough

This pull request extends the product variant creation logic to automatically propagate newly created variants to all channels already assigned to the parent product. The ProductVariantService.createSingle() method now loads the product's assigned channels, derives additional channel IDs, and for each additional channel assigns the variant, creates channel-specific pricing using the channel's default currency, and assigns associated product options and option groups. An E2E test suite validates this by creating a product in the default channel, assigning another channel, then verifying a variant created post-assignment appears in all assigned channels with correct pricing and option relationships.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: assigning newly created product variants to all channels the parent product belongs to, directly addressing issue #4532.
Description check ✅ Passed The description comprehensively explains the root cause, fix implementation, price handling strategy, scope, and includes all required checklist items. Follows the template with detailed technical context.
Linked Issues check ✅ Passed The code changes fully implement the requirements from issue #4532: variants created after channel assignment now propagate to all product channels with proper pricing and option/group assignments.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the variant channel assignment issue. The E2E test validates the fix, and the service modification directly addresses the linked issue requirement without introducing unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/service/services/product-variant.service.ts (1)

506-542: ⚡ Quick win

Batch the per-channel assignments instead of looping per channel.

channelService.assignToChannels(ctx, EntityType, entityId, channelIds) accepts an array of channel IDs and batches them in a single relation operation, so the variant / option-group / option assignments can each be issued once with additionalChannelIds instead of looping. The method deduplicates automatically, making this safe. Only createOrUpdateProductVariantPrice needs to stay per-channel because each channel may have its own defaultCurrencyCode. This reduces DB round-trips on variant creation when a product spans many channels and/or has many options.

♻️ Proposed batched form
-                for (const additionalChannelId of additionalChannelIds) {
-                    const channel = await this.connection.getEntityOrThrow(
-                        ctx,
-                        Channel,
-                        additionalChannelId,
-                    );
-                    await this.channelService.assignToChannels(
-                        ctx,
-                        ProductVariant,
-                        createdVariant.id,
-                        [additionalChannelId],
-                    );
-                    await this.createOrUpdateProductVariantPrice(
-                        ctx,
-                        createdVariant.id,
-                        input.price,
-                        additionalChannelId,
-                        channel.defaultCurrencyCode,
-                    );
-                    // Also assign option groups and options to the target channel
-                    for (const groupId of optionGroupIds) {
-                        await this.channelService.assignToChannels(
-                            ctx,
-                            ProductOptionGroup,
-                            groupId,
-                            [additionalChannelId],
-                        );
-                    }
-                    for (const optionId of optionIds) {
-                        await this.channelService.assignToChannels(
-                            ctx,
-                            ProductOption,
-                            optionId,
-                            [additionalChannelId],
-                        );
-                    }
-                }
+                // Assign the variant to all additional channels in one call
+                await this.channelService.assignToChannels(
+                    ctx,
+                    ProductVariant,
+                    createdVariant.id,
+                    additionalChannelIds,
+                );
+                // Assign option groups and options to all additional channels in one call each
+                for (const groupId of optionGroupIds) {
+                    await this.channelService.assignToChannels(
+                        ctx,
+                        ProductOptionGroup,
+                        groupId,
+                        additionalChannelIds,
+                    );
+                }
+                for (const optionId of optionIds) {
+                    await this.channelService.assignToChannels(
+                        ctx,
+                        ProductOption,
+                        optionId,
+                        additionalChannelIds,
+                    );
+                }
+                // Per-channel price creation (each channel can have its own defaultCurrencyCode)
+                const additionalChannels = await this.connection
+                    .getRepository(ctx, Channel)
+                    .find({ where: { id: In(additionalChannelIds) } });
+                for (const channel of additionalChannels) {
+                    await this.createOrUpdateProductVariantPrice(
+                        ctx,
+                        createdVariant.id,
+                        input.price,
+                        channel.id,
+                        channel.defaultCurrencyCode,
+                    );
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/service/services/product-variant.service.ts` around lines
506 - 542, Replace the per-channel loops with batched assignments: call
this.channelService.assignToChannels once for the createdVariant
(ProductVariant) with the full additionalChannelIds array, once for all option
groups (ProductOptionGroup) using optionGroupIds, and once for all options
(ProductOption) using optionIds; keep the per-channel loop only for
createOrUpdateProductVariantPrice since it needs each channel's
defaultCurrencyCode (use this.connection.getEntityOrThrow to fetch each channel
inside that loop to read channel.defaultCurrencyCode). This removes the repeated
assignToChannels calls while preserving per-channel price creation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/service/services/product-variant.service.ts`:
- Around line 506-542: Replace the per-channel loops with batched assignments:
call this.channelService.assignToChannels once for the createdVariant
(ProductVariant) with the full additionalChannelIds array, once for all option
groups (ProductOptionGroup) using optionGroupIds, and once for all options
(ProductOption) using optionIds; keep the per-channel loop only for
createOrUpdateProductVariantPrice since it needs each channel's
defaultCurrencyCode (use this.connection.getEntityOrThrow to fetch each channel
inside that loop to read channel.defaultCurrencyCode). This removes the repeated
assignToChannels calls while preserving per-channel price creation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 608c97d3-9894-4aed-a313-89d4bb95593e

📥 Commits

Reviewing files that changed from the base of the PR and between f68c183 and 329ad04.

📒 Files selected for processing (2)
  • packages/core/e2e/product-channel.e2e-spec.ts
  • packages/core/src/service/services/product-variant.service.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Additional product variant not visible in assigned channel when created after channel assignment

1 participant