BLK 07Build · 🔵🟣 · ≈4 h · chaincode lab + architecture kata

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.

LO3/LO4 · consortium workflow designLO8 groundwork for the capstone

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.

claims-contract.js · Fabric chaincode (JavaScript)
'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:

deployment · endorsement policy
# 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')"
✦ The architectural payoff

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
  1. 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).

  2. 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.

  3. Swap in the state machine

    Port ClaimsContract above 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.

Brief

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).

Command APIvalidate · sign · submit Append-only ledgerhash-chained · orderedevents, PII by reference StreamKafka topics Projectorsidempotent workers →current + archive views Query sideGraphQL / APIsfast reads rebuild path: replay ledger → regenerate every view Auditor access = read-only ledger verification (hashes + Merkle proofs) — no database access needed. PII lives off-ledger, referenced by id, erasable.
Fig 7.1 The audit-ledger reference architecture — Galileo's shape, generalised. Every requirement in the brief maps to one element.

Kata questions (write answers before peeking at the dossier):

  1. What exactly goes in a ledger event — and what stays out? (Think: DPA erasure, payload size, schema evolution.)
  2. Where does eventual consistency bite users, and what UX/API contract covers the gap between command-accepted and view-updated?
  3. What makes a projector safe to re-run? (Idempotency keys, versioned upserts.)
  4. 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?
  5. Which single failure loses data, and what's the recovery runbook per component?
◐ Compare with the real thing

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):

PatternJobExample in the wild
ESB / API translation layerOld callers keep old contracts; the bus translates to the new backendARGO translating Galileo-style calls to eBao during migration
Adapter per external APIIsolate each external system behind its own adapter classMigration tool's "one adapter per eBao API"; your chain client should mirror it
Anti-corruption layerKeep chain models from leaking into core domain models (and vice versa)Map ClaimTransition events → core claim aggregates at one boundary
ID mapping tablePair internal ids with on-chain ids for the coexistence yearsCentral-DB galileo_reference_id ↔ ebao_reference_id — same trick for chain ids
Outbox / event bridgeReliable 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.