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-b2cdoes 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 lacksreturn.requestis 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’spartyis 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.confirmhere 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.