Lab II — enterprise claims
& the audit-chain kata
Two tracks. Track A: a motor-claims workflow as Fabric chaincode, with the approval rules encoded as endorsement policy. Track B: rebuild Galileo's audit-ledger architecture on paper — the kata that turns you into the person who can review such designs.
ATrack A · Claims workflow as chaincode
Scenario: insurers A and B plus reinsurer R share a motor-claims channel. Business rule: a claim above ₱500k needs both the handling insurer and the reinsurer to endorse the approval. Below that, the insurer alone suffices.
'use strict'; const { Contract } = require('fabric-contract-api'); const STATES = ['FNOL', 'ASSESSING', 'APPROVED', 'PAID', 'REPUDIATED']; const NEXT = { FNOL: ['ASSESSING'], ASSESSING: ['APPROVED','REPUDIATED'], APPROVED: ['PAID'] }; class ClaimsContract extends Contract { async RegisterClaim(ctx, claimId, policyId, amountPhp) { const exists = await ctx.stub.getState(claimId); if (exists && exists.length) throw new Error(`claim ${claimId} exists`); const claim = { claimId, policyId, amountPhp: parseInt(amountPhp, 10), state: 'FNOL', handler: ctx.clientIdentity.getMSPID(), // org that registered it history: [{ at: ctx.stub.getTxTimestamp().seconds.low, to: 'FNOL' }] }; await ctx.stub.putState(claimId, Buffer.from(JSON.stringify(claim))); ctx.stub.setEvent('ClaimRegistered', Buffer.from(claimId)); return JSON.stringify(claim); } async Transition(ctx, claimId, toState) { const raw = await ctx.stub.getState(claimId); if (!raw || !raw.length) throw new Error(`no claim ${claimId}`); const claim = JSON.parse(raw.toString()); // 1 · state machine guard (compare Fig 4.1/4.2 — lifecycle as law) const allowed = NEXT[claim.state] || []; if (!allowed.includes(toState)) throw new Error(`illegal ${claim.state} → ${toState}`); // 2 · role guard: only the handling insurer drives its own claims const caller = ctx.clientIdentity.getMSPID(); if (caller !== claim.handler && caller !== 'ReinsurerRMSP') throw new Error(`${caller} may not act on this claim`); claim.state = toState; claim.history.push({ at: ctx.stub.getTxTimestamp().seconds.low, to: toState, by: caller }); await ctx.stub.putState(claimId, Buffer.from(JSON.stringify(claim))); ctx.stub.setEvent('ClaimTransition', Buffer.from(claimId + '→' + toState)); return JSON.stringify(claim); } async GetClaim(ctx, claimId) { const raw = await ctx.stub.getState(claimId); if (!raw || !raw.length) throw new Error(`no claim ${claimId}`); return raw.toString(); } } module.exports = ClaimsContract;
Where's the "both must sign above ₱500k" rule? Not in the code — in the endorsement policy, set when the chaincode is deployed to the channel:
# standard claims: the handling insurer's org endorses --signature-policy "OR('InsurerAMSP.peer','InsurerBMSP.peer')" # large-loss chaincode (or state-based endorsement on big claims): --signature-policy "AND(OR('InsurerAMSP.peer','InsurerBMSP.peer'),'ReinsurerRMSP.peer')"
Application code validates business logic; the platform enforces who must agree. An insurer literally cannot commit a large-loss approval without the reinsurer's peer co-signing the transaction — the approval matrix from your ops manual, promoted to physics. (Fabric also supports state-based endorsement to switch policy per-claim by amount.)
Lab 7.1 · Run it for real (optional, Docker)
IBM sample- Clone the official sample
github.com/IBM/build-blockchain-insurance-app — a full Fabric insurance network (insurer, shop, repair shop, police) with UI. Follow its README (./build_ubuntu.sh or Docker Compose path).
- Map it to this module
Find their chaincode's claim states and compare with ours; find where the network config defines orgs and policies — that's the four governance rings (M03) as YAML.
- Swap in the state machine
Port
ClaimsContractabove into the sample's chaincode folder and redeploy. Break a rule on purpose; watch the peer reject it.
No Docker? Reading the sample's repo structure still teaches the anatomy: orgs → CAs → peers → channel → chaincode → client apps.
BTrack B · The Galileo audit-ledger kata
Design exercise — paper only, 60–90 min. Rebuild the essential Galileo pattern from requirements, then compare with the real thing.
Requirements
1 · Every business command (issue policy, record payment, approve claim) must leave a tamper-evident, totally-ordered audit record. 2 · Read models must be rebuildable from that record alone. 3 · Reads must be fast and flexible; writes must never race. 4 · A future external auditor should be able to verify history without write access. 5 · PII must remain erasable (DPA).
Kata questions (write answers before peeking at the dossier):
- What exactly goes in a ledger event — and what stays out? (Think: DPA erasure, payload size, schema evolution.)
- Where does eventual consistency bite users, and what UX/API contract covers the gap between command-accepted and view-updated?
- What makes a projector safe to re-run? (Idempotency keys, versioned upserts.)
- Ledger tech choice: private blockchain vs signed append-only log vs ledger database — argue each for this brief; when does multi-node consensus earn its keep?
- Which single failure loses data, and what's the recovery runbook per component?
Galileo's answers: entities as documents with ids (PII in Mongo, not on chain), current/archive collections as the projection pair, Kafka listeners as projectors, a 3-node private chain as the ledger, GraphQL as the query side. Its unsolved gaps — undocumented consistency window, auditor access never externalised — are precisely capstone material.
CBridging to legacy — the adapter discipline
No insurance chain deploys into a greenfield. The integration patterns that recur (all visible in the Galileo→eBao migration):
| Pattern | Job | Example in the wild |
|---|---|---|
| ESB / API translation layer | Old callers keep old contracts; the bus translates to the new backend | ARGO translating Galileo-style calls to eBao during migration |
| Adapter per external API | Isolate each external system behind its own adapter class | Migration tool's "one adapter per eBao API"; your chain client should mirror it |
| Anti-corruption layer | Keep chain models from leaking into core domain models (and vice versa) | Map ClaimTransition events → core claim aggregates at one boundary |
| ID mapping table | Pair internal ids with on-chain ids for the coexistence years | Central-DB galileo_reference_id ↔ ebao_reference_id — same trick for chain ids |
| Outbox / event bridge | Reliable handoff DB-transaction → chain-transaction (no dual-write races) | Write intent to outbox table; a relay submits to chain, records tx hash |
Sources: Hyperledger Fabric docs (chaincode API, endorsement); IBM build-blockchain-insurance-app (GitHub); Galileo dossier (audit pattern, migration integration). Chaincode is teaching material — not audited.