OrderRefresh only refreshes draft orders - OrderRefresh::needsRefresh() gates on state == 'draft', and refresh() itself unconditionally clears all adjustments, re-resolves unit prices against the catalog, and runs every processor. There is no supported way to recompute a single facet of an order after it has been placed.
Two distinct problems motivate this:
1. Refresh firing where it shouldn't. Any code path that reaches refresh() or sets REFRESH_ON_SAVE directly (a custom save, a contrib module, a shipping recalculation) bypasses the draft gate and runs the full, destructive refresh against a placed order. See e.g. #3328162: Non draft orders are refreshed when editing shipments.
2. No opt-in partial refresh. A common need: after an order is placed, a CSR reduces a line item price and tax should recompute - but promotions, availability, and catalog re-pricing must not move. Today that is impossible without reimplementing large parts of OrderRefresh.
Proposed resolution
Introduce a deliberate, opt-in "scoped" refresh alongside the existing behavior, entirely backward compatible:
- Add a scope attribute to the commerce_order.order_processor service tag. Values are order workflow state IDs (e.g. draft, completed), matched literally against the order's current state. These are free-form strings defined in *.workflows.yml or hook_workflows_alter(), not a fixed enum, so custom states work. Absent = ['draft'], preserving current draft-only behavior with zero configuration.
- A scoped refresh runs only the processors registered for that state and clears only the adjustment types they own, leaving all other adjustments, frozen prices, the changed timestamp, and the order email untouched. It is never triggered automatically - the automatic gate (needsRefresh()) stays exactly as locked as today.
- Ship the tax processor opted into placed states as the first consumer.
Backward compatibility
The automatic refresh gate and the full (draft) refresh body are unchanged. The new capability must be delivered without breaking the public API:
- Do not add parameters to OrderRefreshInterface, OrderInterface, or OrderProcessorInterface - that fatals third-party implementers.
- Do not add parameters to the existing OrderRefresh::refresh() / addProcessor() methods - that fatals third-party subclasses (confirmed hard fatal on PHP 8, not a warning).
- Instead expose the scoped API via new methods on a new ScopedOrderRefreshInterface (e.g. refreshScoped(), addScopedProcessor()), deprecating the old interface in favor of it. The new interface should be standalone (not extend the deprecated one) so adopters don't inherit deprecation notices.
Issue fork commerce-3613087
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
zaporylieComment #4
zaporylieMoving to Needs review as it should be ready for testing. I believe BC is maintained and the goals for this tickets are met. Lots of places in the code to touch so MR is assisted by Claude Fable + Opus.