Open Standard · Working Draft
mekaniskt·kontrakt

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; group output foldable only by the same combinators; otherwise mandatory 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.
  • split as 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.