Worked examples · 06 of 07
Marketplace order
A marketplace order with a multi-line cart where one buyer's payment splits three ways across merchant, platform, and drop-ship supplier, with a coupon and per-category VAT.
- SIZE
- medium
- PROFILE
- mk-se-b2c
- STRESSES
- Aggregates over a multi-line cart, a conservation-checked multi-party split, and the facts-vs-money boundary for imports.
This example is the worked answer to a design question: should aggregate functions - sum(cart), sum(cart, x => x.shipping_cost), groupBy(cart, x => x.coupon_applied) - live inside the contract, or should these all be EXTERNAL signals with the contract just validating and executing? The answer is yes and no: some calculation must be internal - the SPLIT between parties, drop shipping, three-party payment - while VAT, tax and coupons are complex and handled by standard external functions. A marketplace order with a real cart: buyer pays once, the money splits three ways (merchant, platform commission, drop-ship supplier), a coupon applies, VAT lands per line category. It forces the expression-grammar question and the split primitive.
The dividing line: provenance, not complexity
The instinct “complex → external” draws the line in the wrong place. The right test is where the knowledge lives:
- External functions return FACTS - values the contract could never derive from its own signed content because they are knowledge that lives outside the agreement and changes with the world: a VAT rate for a goods category, a customs classification, a freight price. §7.6 imports, guarded, never trusted blindly.
- Internal expressions move MONEY - every krona that flows must be recomputable by any party from (contract, log) alone. Totals, discount application, and above all the SPLIT. If the split were external, the contract would be trusting an oracle for the exact thing it exists to judge, the receipt could not show the arithmetic, and pre-validation would die.
So: a complex-but-closed calculation (largest-remainder split across three parties) belongs INSIDE; a trivial-but-open one (today’s VAT rate for category X) belongs OUTSIDE. Complexity is irrelevant; provenance decides. Coupons split down the middle: eligibility (stacking rules, campaign logic - knowledge) may be external or an artifact; application (min(coupon.value, subtotal) - money movement) is always internal.
And “the contract just validates, doesn’t calculate” is a false alternative: to validate a total is to recompute it and compare - §2’s rate check was already exactly that. Validation without calculation is a rubber stamp. The machine needs the aggregate grammar either way; the only question is its vocabulary.
The aggregate grammar (proposed)
Not lambdas. x => x.shipping_cost is developer syntax in a document a kommunjurist must read aloud - and first-class functions are the door Turing-completeness walks back through (a lambda that can hold an if and another sum is a functional language; the linter’s promises get harder with every nesting level). The corpus needs a closed combinator set over bounded declared collections, with field projections only:
sum over cart.lines of line.qty × line.price # projection, not closure
sum over cart.lines of line.shipping-cost
count over cart.lines where line.coupon present
min / max / any / all ... same shape
group cart.lines by line.vat-category # -> a bounded tableOf, foldable
# ONLY by these same combinators
Deterministic and terminating by grammar (inputs are declared, finite collections: payload lines, registers, artifact tables); readable aloud (“the sum over the cart lines of quantity times price”); and closed - no user-defined functions, no recursion, no combinator that takes a rule as argument. group needs one extra honesty: its keys come from data, so rules over groups must carry otherwise (exhaustiveness by escape hatch, since the linter cannot enumerate data-borne keys).
Prior art, thick and comforting: DMN FEEL (the OMG decision-model expression language: built-in sum/count/min/max over lists, deliberately no user functions - lawyers’ decision tables run on it today), CEL comprehension macros (all/exists/map/filter, bounded by construction - Google ships it as the policy-expression standard), SQL aggregates (fifty years of SUM/GROUP BY as the world’s audited-money grammar), and the humble spreadsheet (SUMIF is the most legally-litigated aggregation syntax in history). Every tradition converged on the same answer: closed combinators, no lambdas-as-values.
The split primitive (proposed)
Division corrupts conservation: 100.00 / 3 leaks an öre. Fowler’s Money pattern solved this in 1997 - allocate(ratios): distribute by largest-remainder so the parts sum EXACTLY to the whole, remainder placement deterministic. Adopt it as the one money-splitting verb:
split total by
supplier: goods-cost # absolute first
platform: 12% of subtotal # then ratios
merchant: remainder # exactly one remainder-taker: REQUIRED
# lint: Σ parts == total, structurally - a split cannot mint or vanish an öre
# (the same conservation law §6.4 imposes on transforms)
Three-party payments, drop shipping, marketplace commissions, factoring carve-outs, agent fees - all the same verb. The linter’s promise: every split conserves, every part lands in a declared pool or out-port flow with an ownership verb (§8.3).
The contract (abridged to the new machinery)
mechanical-contract v0 TEMPLATE
id: tpl-marketplace-order
parties:
buyer: party<consumer> from parameters # example 01's identity questions apply
merchant: org from parameters
platform: org 559444-5566 "Marknad AB" sign eIDAS-seal
supplier: org from parameters # the drop-shipper: a real party
# with ports, not a detail
parameters:
cart shape order-lines moved in at genesis # THE CART: multi-line at last
imports:
vat_rate: # facts in...
mode: inline has_side_effects: false
input: shape vat-query output: shape rate
binding: art:se-vat-table track latest-approved
guards: output.rate in { 0%, 6%, 12%, 25% }
coupon_eligibility: # knowledge may be external;
mode: inline has_side_effects: false # APPLICATION stays internal
binding: art:campaign-rules pin v14
guards: output.discount <= coupon.face-value
state:
pool money.order capacity {computed at genesis} stages available|reserved|disbursed
register fulfilments shape despatch-line
ports:
in payment.confirm from psp payload psp-attested-settlement
in fulfil.confirm from supplier payload peppol.despatch-advice
in delivery.confirm from carrier payload carrier-attested-pod
out fulfil.request to supplier on payment # drop shipping: the
out disburse.instruct to psp on delivery # order fans out, the
out receipt.emit to submitter always # money fans out later
rule on genesis: # the cart is judged ONCE, here
each line in cart.lines:
if line.sku not in art:catalog -> reject "unknown item"
subtotal := sum over cart.lines of line.qty × line.price
shipping := sum over cart.lines of line.shipping-cost
discount := min(coupon_eligibility(cart).discount, subtotal) # internal application
vat := sum over (group cart.lines by line.vat-category)
of group.total × vat_rate(group.key) # facts × arithmetic
total := subtotal - discount + shipping + vat
reserve money.order amount total
accept # the receipt shows EVERY line of
# this arithmetic - §2's rule-trace,
# now with aggregates in it
rule on payment.confirm:
if payload.amount != total -> reject "amount mismatch"
move money buyer -> money.order reserved
emit fulfil.request to supplier # the drop-ship leg opens
accept
rule on delivery.confirm: # money moves only on delivered truth
split total by
supplier: sum over cart.lines of line.goods-cost
platform: 12% of subtotal
merchant: remainder
each part: emit disburse.instruct (part, ownership: move) # three disbursements,
accept # conservation linted, each receipted
What this example stresses
| Edge | Spec section |
|---|---|
| Aggregates over a multi-line cart in rules | §9 (grammar extension) |
| Conservation-checked multi-party split | §8.3-8.4 (new verb) |
| Facts-vs-money boundary for imports | §7.6, sharpened |
| Four parties, one contract: drop shipping as declared topology | §3.6, §7.1 |
| Coupon = external eligibility + internal application | §6.3, §7.6 |
Questions this example forced
- The aggregate grammar. As argued above: closed combinators (
sum/count/min/max/any/all/group ... over ... of/by) with field projections, never lambdas-as-values;groupoutput foldable only by the same combinators;otherwisemandatory over data-borne keys. Prior art: DMN FEEL, CEL macros, SQL, spreadsheets. PROPOSED - this grows §9’s “arithmetic” into a specified expression grammar, the single biggest spec addition the examples forced, and it needs a deliberate yes precisely because §9 is the starvation covenant. splitas a vetted money verb. Fowler allocate/largest-remainder, exactly-one remainder-taker, conservation linted, parts bound to flows with ownership verbs. PROPOSED.- The facts/money principle deserves spec text. “External functions return facts; internal expressions move money; every krona recomputable from (contract, log)” is a one-paragraph addition to §7.6 that pre-decides every future “should X be an import?” debate. PROPOSED.
The meta-point: the yes-and-no answer was correct, but the line lands at provenance, not complexity. The contract is not a calculator that validates - it is a judge that recomputes. It aggregates because judging a cart IS aggregating it; it never imports arithmetic it could do itself, and never inlines knowledge that belongs to the world.