Expected margin
The forward-looking margin computed from synthetic downstream transactions, and why it can stay stale after a rule change.
Realised margin needs the downstream transactions that follow an order: the invoice, the fulfillment, the payment. Until those exist, an order has revenue and little else. Expected margin fills that gap by computing what the order's margin should be, from transactions CCC invents rather than imports.
It uses the same arithmetic as contribution margin. The formulas are identical in both code paths, so any difference between an expected and a realised figure is a difference in inputs, never in maths.
What is synthetic about it
CCC prices the sales order itself plus a synthetic invoice, fulfillment and payment, even if real downstream documents already exist. Expected is a separate estimate, not a sum of real documents plus only the missing ones.
That construction makes assumptions, and they are what a reader needs to know before comparing an expected figure to a realised one:
- the synthetic payment method is Unknown, so any rule filtering on payment method behaves differently than it will on the real payment
- the synthetic invoice carries the order's lines and amounts on the order date; the fulfillment uses line cost estimates with their signs reversed
- the synthetic cogs GL mapping can vary by subsidiary; a mapping on the account blacklist produces no expected cogs, so a missing expected cost is not automatically missing source data
- gift-card SKU lines map to a gift-card obligation account and are skipped by the GL blacklist, rather than counted as product revenue
- no returns are simulated, by design. An expected figure is the margin if nothing comes back. That is the intended definition and not a gap to close: the question expected margin answers is what the order is worth as placed, and a return is a later event that realised margin records when it happens
An expected figure is therefore an estimate under stated assumptions, not a prediction that accounts for return behaviour. A divergence between expected and realised on an order that came back is the definition working.
Its own queue, and the staleness this causes
Expected margin has its own queue, cm_expectedimpact_salesorders, separate from realised margin and separate from the batches table. It is a processed = 0 flag on that table, and unlike the realised queue it has no header banner.
It is guarded on the order total. Queueing an order writes its amount_eur_invat and clears processed only when that amount differs from the total the stored figures were calculated from:
processed = IF(amount_eur_invat = <the new amount>, processed, 0)
Reclassifying a day therefore costs one insert per order and recalculates only the orders whose total moved.
A rule change does not reach orders already calculated. Neither does a cost price change. Neither alters the order total, so the guard leaves processed alone and the stored expected figure stays as it was.
An expected figure can therefore be older than the rule that should have produced it. When comparing expected against realised after a configuration change, check that the expected side was actually recomputed before concluding the difference is real.
Reading it beside realised margin
They answer different questions. Expected says what this order should yield under current assumptions; realised says what it has yielded so far given the transactions that actually exist. An order part-way through its life legitimately shows both, and they legitimately differ.
A large gap is worth investigating in this order: has the expected side been refreshed since the last rule change; have returns arrived, which expected margin never modelled; and did the real payment method differ from Unknown.
Related
- Contribution margin
- Calculation rules
- Recalculate margin
- The screen:
/contributionmargin/impact
Evidence
Formulas from app/models/expectedimpact.php:120-123, identical to app/models/impact.php:88-91. The separate amount-guarded queue from app/models/batches.php::setexpectedimpactbatchtodo(). Synthetic construction, Unknown payment method, gift-card treatment and the absence of simulated returns from app/models/expectedimpact.php. Basis: code-checked against the current implementation.
The queue guard is code-checked: app/models/batches.php::setexpectedimpactbatchtodo(), whose ON DUPLICATE KEY UPDATE clears processed only when amount_eur_invat differs.
Omitting returns is the intended definition of expected margin, confirmed by the data owner.
- Type
concept - Status
active - Updated
2026-09-14 - Created
2026-09-13