#5332·vendure

Order totals can be persisted from a stale surcharges snapshot

Author: biggamesmallworldCreated Sep 9, 2026Updated Sep 9, 2026
Labelstype: bug 🐛

Describe the bug

OrderService.applyPriceAdjustments calculates and persists Order.subTotal and Order.subTotalWithTax from the in-memory order.surcharges array. That array is a snapshot taken when the Order was loaded, so a Surcharge added or removed since the load is not reflected in the totals which get written.

The result is a persisted Order whose totals do not match its own surcharge rows. Order.total and Order.totalWithTax are derived from the persisted subTotal columns, so addPaymentToOrder can charge an amount which does not correspond to what the Order actually holds.

Chain:

  • DefaultOrderTaxCalculationStrategy sums order.surcharges into the subtotal (packages/core/src/config/tax/default-order-tax-calculation-strategy.ts:28, :52). OrderLevelTaxCalculationStrategy does the same (packages/core/src/config/tax/order-level-tax-calculation-strategy.ts:102).
  • OrderCalculator.calculateOrderTotals writes the result onto the Order (packages/core/src/service/helpers/order-calculator/order-calculator.ts:376).
  • subTotal and subTotalWithTax are persisted columns (packages/core/src/entity/order/order.entity.ts:170), and total / totalWithTax are computed from them (:245, :269).
  • OrderService.applyPriceAdjustments then saves the Order (packages/core/src/service/services/order.service.ts:2462).

The window is easy to hit, because every cart mutation loads the Order at entry and publishes OrderLineEvent before this save runs. Any blocking event handler which touches surcharges in that window, or any concurrent request doing so, leaves the totals computed from the pre-handler array.

To Reproduce

  1. Register a blocking OrderLineEvent handler which adds a Surcharge to the Order, for example a loyalty or voucher discount of -500.
  2. Call adjustOrderLine on that Order.
  3. Read the Order back.

Expected behavior

The persisted subTotal / subTotalWithTax account for every Surcharge row attached to the Order at the time of the save, or the save is rejected so the caller can retry.

Actual behavior

The surcharge row exists and is attached, but the persisted totals were calculated without it. The Order shows a -500 surcharge and a total which does not include it. A subsequent addPaymentToOrder uses the stale totalWithTax.

Environment

  • @vendure/core version: master
  • Database: driver independent, this is application-level

Additional context

Found while reviewing #5164, which fixes a different symptom of the same stale snapshot: before that PR, saving the Order also detached surcharge rows it did not know about, by orphan-nullifying surcharge.orderId. That is fixed by orphanedRowAction: 'disable' on Surcharge.order, so the rows now survive. The totals can still be stale, which is why this is filed separately.

Possible directions, no strong preference:

  • Reload the surcharge rows immediately before calculateOrderTotals in applyPriceAdjustments, so the totals are computed from what is persisted rather than from the snapshot.
  • Detect the stale write instead of preventing it: optimistic lock on the Order, reject and retry the repricing.

The same question applies to OrderLine and ShippingLine, which are loaded as snapshots and feed the same totals. Surcharges are the easiest to hit because they are the relation a plugin is most likely to mutate from an event handler.