Open Standard · Working Draft
mekaniskt·kontrakt

Worked examples ·  01 of 07

Webshop sale

A website sells a single poster to one buyer, using a signed template plus a per-purchase instance to govern payment, dispatch, delivery, and the statutory 14-day withdrawal right.

SIZE
tiny
PROFILE
mk-se-b2c (proposed)
STRESSES
Template/instance at consumer volume, mandatory statutory ångerrätt, opaque as a GDPR firewall, and executor-attested payloads outside Peppol.

The smallest real mechanical contract we can write. A website sells a poster. There is no cart system, no account, no ERP - a visitor clicks buy on ONE item and a contract must govern what happens next: payment, dispatch, delivery, and the 14-day ångerrätt the law gives the buyer whether the seller likes it or not. It should be tiny and it should be fast - and it is: the happy path is a genesis event plus four submissions, five receipts, done.

Profile: mk-se-b2c - which does not exist yet. Writing this example is what proves it must - an open design question in the working draft.

Shape: template + instance (§6.3). The shop signs ONE template; every purchase is a genesis event binding four parameters. A thousand sales a day = a thousand tiny logs referencing one governed artifact.

The template (what the shop signs once)

mechanical-contract v0  TEMPLATE
id:       tpl-webshop-sale
profile:  mk-se-b2c                        # binds distansavtalslagen (2005:59) +
prose:    kopvillkor-v3.pdf  sha256:41c9...  # konsumentkoplag (2022:260) defaults
annex:    [ "§8 Personuppgifter (GDPR)", "§9 Tvist (ARN)" ]   # soft terms

parameters:                                # unbound slots - the instance fills these
  item      shape order-line               # ONE line. no cart = the basket IS one line
  price     money SEK
  buyer     party<consumer>                # open question: what identity scheme?
  delivery  opaque                         # sealed at genesis: address never queryable

parties:
  seller:  org 559123-4567 "Print AB"   sign eIDAS-seal        # signs the TEMPLATE
  buyer:   from parameters              sign payment-authorization   # open question: signature
                                                                     # by conduct
money:
  currency SEK
  vat from import vat_rate
  rounding per-line half-up 0.01

state:
  pool item.sold        capacity 1  named            # the poster: one, identity-bearing
  pool money.price      capacity {price} stages available|reserved|disbursed
  bind order            shape order-line  at genesis  moved in
  bind delivery-address opaque            at genesis  moved in   # §8.4: storable,
                                                                 # never branchable-on
imports:
  vat_rate:
    mode: inline   has_side_effects: false
    input: shape order-line    output: shape rate
    binding: art:se-vat-table  track latest-approved  sha256:77aa...
    guards: output.rate in { 6%, 12%, 25% }

ports:
  in  payment.confirm   from psp        payload psp-attested-settlement   # open question: card
                                                                          # rails ≠ ISO 20022
  in  dispatch.notify   from seller     payload peppol.despatch-advice
  in  delivery.confirm  from carrier    payload carrier-attested-pod
  in  return.request    from buyer      payload signed-text        # MANDATORY per profile:
  in  return.receive    from seller     payload signed-decision    # a b2c template without
  out refund.instruct   to psp          on accepted return         # these ports must not lint
  out receipt.emit      to submitter    always

cadence:
  due dispatch.notify   within 5 working-days of payment.confirm
  due delivery          within 30 days of genesis        # konsumentkoplag default
  return-window         14 days from delivery.confirm    # distansavtalslagen 2 kap 10 §
  due refund            within 14 days of return.request accepted

rule on payment.confirm:
  if payload.amount != price + vat        -> reject "amount mismatch"
  move money buyer -> money.price reserved      # linear: paid once
  accept                                        # opens the dispatch due

rule on dispatch.notify:
  if no accepted payment.confirm          -> reject "not paid"
  draw pool item.sold                           # the poster leaves the shelf
  accept

rule on delivery.confirm:
  accept                                        # opens return-window (fork: one event,
                                                # one new open obligation - §3.7)
rule on return.request:
  if not within return-window             -> reject "angerfrist passed"
  accept                                        # opens the refund due

rule on return.receive:                         # goods came back
  emit refund.instruct amount price + vat       # money.price reserved -> back to buyer
  accept

escalation:
  on obligation-breached:                       # late dispatch, late refund
    level 1  notify both                 resolve-within 5 working-days
    level 2  formal dispute: ARN per prose §9   # Allmänna reklamationsnämnden -
  finally:                                      # the b2c eskaleringstrappa is short
    reklamationsratt 3 years per konsumentkoplag  # survival: outlives the log's
                                                  # happy-path closure

The instance (what a purchase creates)

genesis mk-se-b2c-2027-8842107:
  template  tpl-webshop-sale @ v3  sha256:41c9...    # pinned forever - "we updated
  bind item      { sku: poster-a2-lanta, qty: 1 }    #  our terms" can never reach
  bind price     249.00 SEK                          #  this sale retroactively (§6.3)
  bind buyer     party<consumer> ref psp:pi_3Oa...   # pseudonymous: identity held by the PSP
  bind delivery  opaque sha256:9c1f...               # address sealed in the body
  signed: seller (template seal) + buyer (SCA authorization, dynamically linked)

The whole instance is nine lines. That is the million-customer economics of §6.3, demonstrated: the contract governing this sale is a parameter tuple plus a hash.

The happy path, as receipts

# Event Verdict State after
r-0 genesis ACCEPTED item 1/1, money 0, dues: payment
r-1 payment.confirm (PSP) ACCEPTED money reserved, due: dispatch ≤ 5 wd
r-2 dispatch.notify ACCEPTED item drawn, due: delivery ≤ 30 d
r-3 delivery.confirm (carrier) ACCEPTED return-window open 14 d
r-4 (window lapses - no event needed: expiry is event-math, §3.4) - money disbursed, contract closes, finally survives

Five receipts. The receipt chain IS the order confirmation, the proof of delivery, and the bookkeeping voucher - nothing else needs to exist. Pre-validation works for consumers too: the shop’s own frontend runs the template before showing the buy button, so an out-of-stock or mispriced purchase is structurally unsubmittable.

What this example stresses

Edge Spec section
Template/instance at consumer volume, genesis as the purchase itself §6.3
The smallest possible port manifest that is still legally complete §1, §9
Mandatory statutory terms (ångerrätt) meeting “profiles constrain, never extend” §6.1
opaque as the GDPR firewall; envelope/body detachment as erasure §8.4, §12
Executor-attested payloads outside the Peppol world (PSP, carrier) §3.6, §5.5

Questions this example forced

  • mk-se-b2c does not exist, and consumer law is mandatory law. Distansavtalslagen cannot be contracted away. Proposed resolution: a profile CAN mandate ports and dues without violating “constrain, never extend” - refusing to lint a b2c template that lacks return.request is a constraint, exactly like refusing an invoice without a BT-13. The profile ships the mandated blocks as template defaults.
  • What is a consumer, as a party? §8.4’s party is ISO 6523/orgnr-shaped. A consumer is a natural person whose personnummer must NOT sit in a replayable log. Proposed: party<consumer> = a pseudonymous kinded id bound to a verified payment credential, with real identity held by the PSP/bank (who already have KYC duties) and resolvable only under a declared disclosure scope (dispute, court).
  • What is the buyer’s signature? Nobody BankID-signs a 249 SEK poster. Proposed: the profile binds the buyer’s signature slot to PSD2 SCA with dynamic linking - the authorization code cryptographically binds (amount, payee), which is signature-shaped in exactly the dimension the contract needs. Avtal genom konkludent handlande is the legal frame; whether SCA evidence satisfies it in dispute practice is a real lawyer question, flagged OPEN.
  • Card rails don’t speak ISO 20022 to merchants. payment.confirm here is a PSP webhook, not a pain.002. Proposed: §5.5 tier-2 ingest attestation, named honestly - the PSP is an executor-party whose attested settlement events are the payload standard, per profile. Weaker than a bank-signed camt entry; declared as weaker.
  • GDPR erasure vs the append-only log. The buyer has erasure rights; the log is immutable by covenant. Proposed: personal data may ONLY live in detachable bodies (never in envelopes, never in queryable state - the linter enforces it for any party<consumer> contract), so erasure = body detachment + salt destruction, chain intact. This turns §12’s redaction machinery from a disclosure feature into a compliance requirement - promotes it from v2-nice to v2-necessary.

Status: example, not roadmap. What it proves: the two-person-AB bar of §6.2 extends down to a no-lawyer, no-procurement, sub-second consumer purchase without the core growing a single primitive.

The contract, in mk

The full fixture in the working v0.1 grammar - Swedish clauses governing, English mk blocks:

---
mk: v0
id: tpl-webshop-sale
title: Köpvillkor - enstaka köp utan kundvagn
prose-lang: sv (governs)
profile: mk-se-b2c
template: true
---

# Köpvillkor Print AB

## § 1 Parterna och avtalets ingående  {#klausul-1 .implemented}

Säljare är Print AB, org.nr 559123-4567. Köpare är den konsument som
genomför köpet. Avtal ingås när köparen genomför betalningen; betalningen
utgör köparens accept av dessa villkor. [law:se/sfs/1915:218#p1]

```mk
parameters:
  item      shape: order-line      # ingen kundvagn: EN rad är hela korgen
  price     money SEK
  buyer     party<consumer>
  delivery  opaque                 # adress förseglad: aldrig frågbar (§8.4)

parties:
  seller  org: 559123-4567 "Print AB"   sign: eIDAS-seal
  buyer   from: parameters              sign: payment-authorization
  implements: [#klausul-1]
  caption: "Betalningen utgör köparens accept av villkoren."

§ 2 Pris och betalning {#klausul-2 .implemented}

Priset anges vid köpet inklusive mervärdesskatt. Betalning sker i förskott via betalningsförmedlaren.

money:
  currency: SEK
  vat: from imports.vat_rate
  rounding: per-line half-up 0.01

state:
  pool item.sold     capacity: 1 named
  pool money.price   capacity: {price}  stages: available|reserved|disbursed
  bind order         shape: order-line  at: genesis  moved-in
  bind delivery      opaque             at: genesis  moved-in

imports:
  vat_rate:
    mode: inline   side-effects: false
    binding: art:se-vat-table@latest-approved
    guards: output.rate in { 6%, 12%, 25% }
  implements: [#klausul-2]

§ 3 Leverans {#klausul-3 .implemented}

Varan skickas inom fem arbetsdagar från mottagen betalning och ska levereras senast trettio dagar efter köpet. [law:se/sfs/2022:260#kap5-p1]

ports:
  in  payment.confirm   from: psp      payload: psp-attested-settlement
  in  dispatch.notify   from: seller   payload: peppol.despatch-advice
  in  delivery.confirm  from: carrier  payload: carrier-attested-pod
  out receipt.emit      to: submitter  always
  # return.request / return.receive / refund.instruct: contributed by
  # module:std/se/angerratt@v1 (see § 4) - visible here after flattening

cadence:
  | obligation        | window                                  |
  |-------------------|-----------------------------------------|
  | dispatch.notify   | within 5 working-days of payment.confirm |
  | delivery          | within 30 days of genesis                |
  implements: [#klausul-3]
  caption: "Varan skickas inom 5 arbetsdagar och levereras inom 30 dagar."

§ 4 Ångerrätt {#klausul-4 .implemented}

Köparen har rätt att frånträda avtalet inom fjorton dagar från den dag varan mottogs, enligt lag om distansavtal. [law:se/sfs/2005:59#kap2-p10]

use module:std/se/angerratt@v1        # STANDARD module: tag resolves to hash at
  with: { window: 14 days,            # flatten; mk-se-b2c refuses templates
          refund-amount: price + vat, # lacking it
          buyer: party:buyer, seller: party:seller }
  implements: [#klausul-4]

rule on payment.confirm:
  require payload.amount == price + vat
    else reject "amount mismatch"
  move money party:buyer -> money.price reserved
  accept

rule on dispatch.notify:
  require accepted payment.confirm exists
    else reject "not paid"
  draw item.sold
  accept

§ 5 Reklamation och tvist {#klausul-5 .soft}

Köparen har reklamationsrätt i tre år enligt konsumentköplagen [law:se/sfs/2022:260#kap4]. Tvist som inte kan lösas mellan parterna kan prövas av Allmänna reklamationsnämnden (ARN); säljaren åtar sig att följa ARN:s rekommendationer.

use module:std/se/dispute-arn@v1      # second standard module: the escalation
  with: { notify-window: 5 working-days }   # ladder arrives by union-merge
  implements: [#klausul-5]

escalation:
  finally:                            # host keeps its own survival clause -
    reklamationsrätt: 3 years   law:se/sfs/2022:260#kap4    # modules ADD, the
  implements: [#klausul-5]                                  # host's terms remain

§ 6 Personuppgifter {#klausul-6 .soft}

Säljaren behandlar personuppgifter enligt gällande dataskyddslagstiftning. Leveransadress lagras förseglad och raderas ur läsbar form när avtalets samtliga förpliktelser fullgjorts.