Skip to content

fix(core): Unassigning shipping lines from a channel breaks active orders#4494

Open
kwerie wants to merge 24 commits into
vendurehq:masterfrom
kwerie:fix/shipping-line-deletion
Open

fix(core): Unassigning shipping lines from a channel breaks active orders#4494
kwerie wants to merge 24 commits into
vendurehq:masterfrom
kwerie:fix/shipping-line-deletion

Conversation

@kwerie
Copy link
Copy Markdown
Contributor

@kwerie kwerie commented Mar 11, 2026

Description

When unassigning a shipping method from a channel it breaks currently active orders because a reference on the shipping method still remains on the order, but the resolver cannot find the shipping method because it is removed from the channel and the schema has ShippingMethod declared as non-nullable.

Breaking changes

No

Screenshots

See #4492

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

Fixes #4492, relates to #4486

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 11, 2026

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

Project Deployment Actions Updated (UTC)
vendure-storybook Error Error May 29, 2026 11:53am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a filterOnChannel flag to ShippingMethodService.findOne to allow bypassing channel-scoped lookups, updates ShippingLineEntityResolver to call findOne with channel filtering disabled for certain resolutions, and changes RequestContext.copy to accept an optional Channel override. OrderService now registers a blocking ChangeChannelEvent handler to remove shipping lines from active orders when a shipping method is unassigned from a channel and reapplies price adjustments. Adds e2e test coverage and test-only GraphQL mutations for channel and payment-method management.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • michaelbromley
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: fixing the issue where unassigning shipping methods from a channel breaks active orders.
Description check ✅ Passed The PR description covers the key aspects: problem statement, breaking changes confirmation, linked issues, and completed checklist items. All required sections are present and adequately filled.
Linked Issues check ✅ Passed The code changes directly address the primary objective: preventing active orders from breaking when shipping methods are unassigned by removing invalid shipping lines and recalculating prices.
Out of Scope Changes check ✅ Passed All changes are scope-aligned: ShippingMethodService updates enable channel-unscoped lookups for historical orders, OrderService handles removal of shipping lines on channel unassignment, RequestContext.copy supports channel context switching, and ShippingLineEntityResolver passes filterOnChannel=false for proper resolution.
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

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/service/services/shipping-method.service.ts (1)

317-336: Stale order totals after shipping line removal require explicit recalculation.

The method removes ShippingLine records directly from the database without recalculating the order's totals (shipping, shippingWithTax, total, totalWithTax). Affected orders will retain stale shipping cost values until the next cart modification triggers applyPriceAdjustments().

Contrast this with OrderCalculator.applyShipping(), which recalculates shipping as part of the price adjustment workflow. Consider whether orders should have their totals recalculated immediately after shipping line removal, or if eventual recalculation on next modification is acceptable design. If immediate recalculation is needed, injecting OrderService and calling a recalculation method would be required, though this adds complexity and potential circular dependency concerns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/service/services/shipping-method.service.ts` around lines
317 - 336, The removal of ShippingLine rows in
removeShippingMethodFromActiveOrders leaves orders with stale totals because no
recalculation is triggered; after removing ShippingLine entities (ShippingLine)
you should trigger an immediate price recalculation for each affected order by
invoking the order-level recalculation routine instead of relying on eventual
updates. Inject or obtain the OrderService (or a method that wraps
OrderCalculator.applyShipping/applyPriceAdjustments) into the class, collect the
distinct order ids from shippingLinesToRemove (or from the loaded relations),
remove the ShippingLine entities, then call the OrderService's recalculation
method (e.g. orderService.recalculateOrderTotals(order.id) or
orderService.applyPriceAdjustments(order.id)) for each affected order; if adding
OrderService would create a circular dependency, emit an event (e.g.
"order.shippingRemoved") with the order ids and handle recalculation in the
OrderService to avoid the cycle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/e2e/shipping-method.e2e-spec.ts`:
- Line 526: Replace the placeholder issue reference in the inline comment "//
https://github.com/vendure-ecommerce/vendure/issues/XXXX" with the real issue
number; update it to "//
https://github.com/vendure-ecommerce/vendure/issues/4492" so the comment points
to the correct issue (search for that exact comment string in
shipping-method.e2e-spec.ts and change XXXX to 4492).

---

Nitpick comments:
In `@packages/core/src/service/services/shipping-method.service.ts`:
- Around line 317-336: The removal of ShippingLine rows in
removeShippingMethodFromActiveOrders leaves orders with stale totals because no
recalculation is triggered; after removing ShippingLine entities (ShippingLine)
you should trigger an immediate price recalculation for each affected order by
invoking the order-level recalculation routine instead of relying on eventual
updates. Inject or obtain the OrderService (or a method that wraps
OrderCalculator.applyShipping/applyPriceAdjustments) into the class, collect the
distinct order ids from shippingLinesToRemove (or from the loaded relations),
remove the ShippingLine entities, then call the OrderService's recalculation
method (e.g. orderService.recalculateOrderTotals(order.id) or
orderService.applyPriceAdjustments(order.id)) for each affected order; if adding
OrderService would create a circular dependency, emit an event (e.g.
"order.shippingRemoved") with the order ids and handle recalculation in the
OrderService to avoid the cycle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 32811309-1555-4a16-ba09-f1cceb81056f

📥 Commits

Reviewing files that changed from the base of the PR and between 9c19aaa and dbb35d6.

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

Comment thread packages/core/e2e/shipping-method.e2e-spec.ts Outdated
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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/e2e/shipping-method.e2e-spec.ts`:
- Around line 559-582: The current test only verifies removal of shipping lines
in the same channel; add a regression case that ensures deletion cleans up
orders across channels by: creating/ensuring a second Channel, assigning the
same shipping method to that channel (use whatever mutation/utility you already
have to add the shipping method to another channel), creating an active order in
that second channel via shopClient configured to that channel (use
shopClient.asAnonymousUser() or channel-aware client), applying the shipping
method to that order with SET_SHIPPING_METHOD, then calling
DELETE_SHIPPING_METHOD via adminClient (which triggers
removeShippingMethodFromActiveOrders(ctx, ctx.channelId, id)) and finally
querying GET_ACTIVE_ORDER for the second channel to assert its shippingLines are
removed; reference the existing test symbols SET_SHIPPING_METHOD,
DELETE_SHIPPING_METHOD, GET_ACTIVE_ORDER, shopClient, adminClient and the helper
removeShippingMethodFromActiveOrders to locate where to add this new case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a2dddd37-920b-4ec3-af42-414b97174dde

📥 Commits

Reviewing files that changed from the base of the PR and between dbb35d6 and dfcbf6e.

📒 Files selected for processing (1)
  • packages/core/e2e/shipping-method.e2e-spec.ts

Comment thread packages/core/e2e/shipping-method.e2e-spec.ts Outdated
@kwerie kwerie marked this pull request as draft March 11, 2026 10:33
@kwerie kwerie changed the title fix(core): Unassigning or deleting shipping lines from a channel breaks active orders fix(core): Unassigning shipping lines from a channel breaks active orders Mar 11, 2026
@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 11, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/service/services/shipping-method.service.ts`:
- Around line 321-334: After removing the ShippingLine entities, iterate the
unique parent orders from shippingLinesToRemove and trigger the order
totals/promotions recalculation for each so shipping, shippingWithTax, total and
promotion state are updated; specifically, after await
this.connection.getRepository(ctx, ShippingLine).remove(shippingLinesToRemove)
collect the distinct order objects from shippingLinesToRemove and call the
existing order recalculation method (e.g. OrderService.recalculateOrderTotals,
OrderCalculator.recalculateOrder or whichever project function handles order
aggregates) for each order, then persist any changes to the Order repository.
- Around line 251-252: The soft-delete path in ShippingMethodService
(softDelete) does not invoke removeShippingMethodFromActiveOrders, so active
orders can still reference the deleted shipping method; update the softDelete
implementation in shipping-method.service.ts to call
this.removeShippingMethodFromActiveOrders(ctx, null, shippingMethodId) (or
undefined) so the cleanup runs without a channel filter, and ensure
removeShippingMethodFromActiveOrders correctly treats a null/undefined channelId
as “all channels” and removes the shipping method from any active orders prior
to or immediately after marking the method as soft-deleted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51bbf822-88fc-4e3e-9ef6-23a7822658d9

📥 Commits

Reviewing files that changed from the base of the PR and between dfcbf6e and a8ed3be.

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

Comment thread packages/core/src/service/services/shipping-method.service.ts Outdated
Comment thread packages/core/src/service/services/shipping-method.service.ts Outdated
@kwerie kwerie marked this pull request as ready for review March 11, 2026 12:04
Copy link
Copy Markdown
Member

@michaelbromley michaelbromley left a comment

Choose a reason for hiding this comment

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

Thanks for tackling this — the bug is real and the fix direction makes sense. Cleaning up shipping lines proactively on unassign is the right approach over trying to handle nulls in the resolver.

A couple of things to address though, one of which is a blocker:

1. Order totals go stale after removing the shipping line (blocker)

When you remove the ShippingLine rows, the parent Order's shipping and shippingWithTax columns still hold the old values — they're persisted @Money() columns, not derived on-the-fly. So after this runs, the customer sees no shipping lines but the order total still includes the shipping cost. It won't self-correct until the next applyPriceAdjustments() (e.g. adding another item to the cart).

You'll need to either zero out shipping/shippingWithTax on affected orders and save them, or trigger a proper order recalculation after the removal. Given that pulling in OrderService here would create a circular dependency, the simplest option might be to reset those columns directly and save the order.

2. Test should assert on order totals

The test checks that shippingLines is empty, which is good — but it should also verify that shipping, total, and totalWithTax are correct after the operation. That would have caught the stale totals issue.

See inline comments for the specifics.

@michaelbromley
Copy link
Copy Markdown
Member

Had a closer look at this and there's a broader issue to consider beyond active orders.

The non-active order problem

The ShippingLineEntityResolver resolves shippingMethod by calling findOneInChannel() scoped to ctx.channelId. This applies to all orders — active, placed, completed, fulfilled, etc. So if you unassign a shipping method from a channel, viewing any historical order that used that method will also blow up, because shippingMethod is declared as ShippingMethod! (non-nullable) in the schema.

This is arguably worse than the active order case, since historical orders are read-only and there's nothing the customer or admin can do to "fix" them.

Two separate fixes needed

1. Active orders — use OrderCalculator.applyPriceAdjustments()

The current approach of removing shipping lines is on the right track, but the order totals need to be recalculated afterwards. Rather than manually resetting shipping/shippingWithTax columns, the OrderCalculator.applyPriceAdjustments() method should be used — its internal applyShipping() logic already handles removing invalid/ineligible shipping lines and recalculating totals properly. Rolling your own recalculation here would duplicate that logic and bypass any custom strategies people might have configured.

2. Non-active orders — fix at the resolver/query level

For placed/completed orders, we should NOT be modifying shipping lines — that's historical data. The fix here is that the ShippingLineEntityResolver (or the underlying findOne call) should be able to resolve shipping methods that have been unassigned from the channel when the context is a historical order. Same idea as passing includeDeleted: true for soft-deleted methods — channel assignment shouldn't prevent resolving a shipping method that's referenced by an existing order.

@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 12, 2026

@michaelbromley Thanks for the detailed feedback! Good call on the historical orders angle, I hadn't considered that. I'll rework the approach along those lines. 😄

…ed from channel

Instead of directly removing shipping lines, subscribe to
ChangeChannelEvent and use applyPriceAdjustments() to properly
recalculate totals. Also allow the ShippingLine resolver to look up
shipping methods across channels so historical orders still resolve
correctly.

Fixes vendurehq#4494
@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 13, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/e2e/shipping-method.e2e-spec.ts (1)

655-657: Use one source of truth for payment method code.

Line 657 hardcodes 'test-payment-method' while Line 694 uses testSuccessfulPaymentMethod.code. Reuse the same constant in both places to prevent drift.

♻️ Suggested change
-                    code: 'test-payment-method',
+                    code: testSuccessfulPaymentMethod.code,

Also applies to: 694-694

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/e2e/shipping-method.e2e-spec.ts` around lines 655 - 657, The
payment method code is hardcoded in the CREATE_PAYMENT_METHOD call; replace the
literal 'test-payment-method' with a single source of truth (either use the
existing testSuccessfulPaymentMethod.code value or introduce a shared constant
like testPaymentMethodCode) so both the CREATE_PAYMENT_METHOD invocation and the
later reference (testSuccessfulPaymentMethod.code) use the same identifier;
update the adminClient.query(CREATE_PAYMENT_METHOD, { input: { code: ... }})
call to reference that shared symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/e2e/shipping-method.e2e-spec.ts`:
- Line 39: You're importing CREATE_PAYMENT_METHOD from another spec which causes
that spec's top-level describe to run as a side effect; remove the import from
payment-method.e2e-spec and either inline the CREATE_PAYMENT_METHOD GraphQL
mutation in shipping-method.e2e-spec.ts (e.g., define the gql mutation constant
in this file) or move the mutation (and any fragments like PaymentMethod) into a
shared GraphQL definitions module and import from that module instead so no spec
files are imported; update references in shipping-method.e2e-spec.ts to use the
new local/shared CREATE_PAYMENT_METHOD constant.

---

Nitpick comments:
In `@packages/core/e2e/shipping-method.e2e-spec.ts`:
- Around line 655-657: The payment method code is hardcoded in the
CREATE_PAYMENT_METHOD call; replace the literal 'test-payment-method' with a
single source of truth (either use the existing testSuccessfulPaymentMethod.code
value or introduce a shared constant like testPaymentMethodCode) so both the
CREATE_PAYMENT_METHOD invocation and the later reference
(testSuccessfulPaymentMethod.code) use the same identifier; update the
adminClient.query(CREATE_PAYMENT_METHOD, { input: { code: ... }}) call to
reference that shared symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50a1c734-78e2-4f97-bf7e-7f7f30dcca55

📥 Commits

Reviewing files that changed from the base of the PR and between 7975d82 and d97b39c.

📒 Files selected for processing (1)
  • packages/core/e2e/shipping-method.e2e-spec.ts

Comment thread packages/core/e2e/shipping-method.e2e-spec.ts Outdated
@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 17, 2026

I've added an optional channel param to RequestContext.copy(). This way applyPriceAdjustments calculates for the right channel while keeping the transaction from event.ctx. Happy to change the approach if you'd rather not touch copy().

@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 17, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Mar 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kwerie
Copy link
Copy Markdown
Contributor Author

kwerie commented Apr 10, 2026

Hi @michaelbromley, gentle ping on this one. Could you take a look when you have a moment? :)

@michaelbromley
Copy link
Copy Markdown
Member

@kwerie Hi, thanks for making the changes. I’ve got this on the radar for the team to review at the next opportunity. Thanks for your patience.

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.

Unassigning shipping methods from a channel will break active orders

2 participants