Order totals can be persisted from a stale surcharges snapshot
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:
DefaultOrderTaxCalculationStrategysumsorder.surchargesinto the subtotal (packages/core/src/config/tax/default-order-tax-calculation-strategy.ts:28,:52).OrderLevelTaxCalculationStrategydoes the same (packages/core/src/config/tax/order-level-tax-calculation-strategy.ts:102).OrderCalculator.calculateOrderTotalswrites the result onto the Order (packages/core/src/service/helpers/order-calculator/order-calculator.ts:376).subTotalandsubTotalWithTaxare persisted columns (packages/core/src/entity/order/order.entity.ts:170), andtotal/totalWithTaxare computed from them (:245,:269).OrderService.applyPriceAdjustmentsthen 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
- Register a blocking
OrderLineEventhandler which adds a Surcharge to the Order, for example a loyalty or voucher discount of -500. - Call
adjustOrderLineon that Order. - 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
calculateOrderTotalsinapplyPriceAdjustments, 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.
Source: vendurehq/vendure