# Architecture views

Four views of genalpha-bss: what the components are, how tenancy works, how an order becomes
a bill, and how events move. All diagrams are Mermaid and render natively on GitHub.

## 1. Component map (ODA framing)

Channels talk to one gateway; the gateway routes TMF paths to components; components talk to
each other machine-to-machine (client credentials of the acting tenant) and publish domain
events to Kafka. Every component owns its own database. AI shopping agents (ChatGPT-class over
ACP, Claude-class over MCP) enter through the same gateway, behind a per-tenant
`agent-commerce: off | discovery | full` gate — dark by default, and checkout only ever on a
delegated, commerce-scoped token. AI **digital workers** (Hermes-class, any MCP runtime) enter
the same way but as badge-hired STAFF: a revocable `digital-worker` grant, a task queue derived
from real backlogs on the intelligence component, verified completion, and approvals a human
executes with their own token. With the opt-in **workforce package** deployed, the loop CLOSES:
its `worker-controller` (the only holder of spawn-rights — never the core BSS) makes hire = a
running container with the badge injected, fire = revoke + stop, and the worker's BRAIN
(`worker-ai-*` in the live registry) hot-swappable by config — the controller rolls the workers,
free because claims are self-expiring leases. And a tenant can WRAP a legacy BSS (suite #67):
three per-tenant seams federate its catalog, hand fulfilment back to its queue, and let the
workforce work its incident backlog — legacy stays the master, never two writers. AI ANSWER-ENGINE crawlers (GPTBot-class, which
execute no JavaScript) get a third face of the same catalog: the gateway dual-serves shop URLs
by User-Agent into bot-readable HTML with schema.org JSON-LD, behind a per-tenant
`ai-visibility: open | search-ai | search-only | dark` switch executed at robots.txt
(suites #68 and #246) — `search-ai` is "AI search yes, training no", and `dark` adds a
noindex header because a Disallow leaves a bare URL indexable.

The platform also reaches **operator-to-operator**. On **open-access fibre** (opening now across
the Nordics) a retail order's fiber component can ride a third party's network: the orchestrator
buys the **wholesale access input** the component `reliesOn` from the owner over a **MEF LSO
Sonata** adapter (async callback), and the same platform can be the owner on the *other* side —
a **sibling tenant** accepts inbound Sonata `serviceOrder`s, activates, notifies the retailer and
settles, so seeker and provider meet **cross-tenant through the Sonata face**. Partners work this
from the **`partner-console`** channel. And because a campaign-day crowd is all anonymous and all
identical, the gateway carries a **browse-path edge cache**: anonymous catalog GETs are served
from a per-tenant `LocalResponseCache` (keyed by `X-Tenant-Id`, honouring the catalog's
`Cache-Control`/`no-store` contract) and the cache is **scoped to the catalog route alone** — a
logged-in cart, order or bill is never cached.

```mermaid
flowchart TB
    subgraph Channels["Engagement — channels (white-labeled per tenant by hostname)"]
        SHOP["storefront /shop"]
        BIZ["business-console /biz\n(org admin + member my-page)"]
        CSR["csr-console /csr"]
        ADMIN["admin-console /console"]
        APP["mobile-app /app\n(Expo RN: web + iOS/Android)"]
        DEALER["dealer-app /dealer-app\n(retail + telesales)"]
        PARTNER["partner-console /partner\n(wholesale / open-access:\naccess-seeker + provider desk)"]
    end

    AGENTS(["AI shopping agents\nChatGPT/Perplexity (ACP) · Claude (MCP)\nfeed + delegated checkout"])

    CRAWLERS(["AI answer-engine crawlers\nGPTBot · ClaudeBot · PerplexityBot\n(no JS — read HTML only)"])

    WORKERS(["AI digital workers\nHermes / any MCP runtime\nbadge-hired staff: tickets ·\nunapplied cash · approvals"])

    WCTL["worker-controller\n(opt-in workforce package —\nthe ONLY holder of spawn-rights)"]

    GW["API Gateway :8080\nHost → tenant (X-Tenant-Id)\ntwo-ring rate limit (Redis)\nagent-commerce gate (off|discovery|full)\nbrowse-path edge cache (catalog route only)\nper-channel tenant-config.js"]

    subgraph Party["Party management"]
        PARTY["party-account\nTMF632/666/669 · GDPR export/erase"]
        ROLES["user-roles TMF672\n(over the tenant's IdP)"]
    end

    subgraph Core["Core commerce"]
        CAT["product-catalog TMF620\n+ ACP product feed\n+ GEO bot pages · sitemap · robots"]
        ORD["product-ordering TMF622"]
        INV["product-inventory TMF637"]
        CART["shopping-cart TMF663\n+ ACP checkout_sessions"]
        QUAL["qualification TMF679"]
        ADDR["geographic-address TMF673"]
        APPT["appointment TMF646"]
        STOCK["product-stock TMF687"]
        CHOICE["benefit-allowance\n(household Choices: plans grant, benefits spend)"]
        AGR["agreement TMF651"]
        PROMO["promotion TMF671"]
        REC["recommendation TMF680"]
        DOC["document TMF667\n(content seam: in-row / S3 / Azure Blob;\nper-tenant reference mode: bring-your-own CMS/DAM)"]
        QUOTE["quote TMF648 + TMF699\n(B2B quotes + sales pipeline:\nleads → staged opportunities → weighted forecast)"]
        PORT["porting\n(MNP, per-country gateway)"]
    end

    subgraph Revenue["Revenue"]
        PAY["payment TMF676"]
        VAULT["payment-method TMF670"]
        BILL["billing TMF678\n(crash-resumable run + ledger\n· statutory collections ladder\n· bill-distribution channel chain)"]
        USAGE["usage TMF635/677\n(+ policy layer: pools ·\nspend meters · travel passes)"]
        DEVCOM["device-commerce\n(agreements · financing port ·\ntrade-in · 14-day withdrawal)"]
        MIGRATE["base-migration\n(rehearsal-gated cohort moves\n→ TMF622 modify orders)"]
    end

    subgraph Production["Production (OSS, thin)"]
        SOM["service-orchestration
TMF641/638/640/685 · dealer + telesales"]
        ASSUR["assurance
TMF642/656"]
    end

    subgraph Decisioning["Decisioning & knowledge — rules as data, AI as a governed seam"]
        POLICY["policy\norder rules · dynamic pricing · personalization\n(JSON-logic, no redeploy)"]
        KNOW["knowledge\nFTS + pgvector semantic search"]
        INSIGHT["insight\nconsent spine · web/GA seam\nnext-hit session personalization"]
        AI["intelligence — the AI control plane\ncopilots · advisor · churn · For-you\nworkforce queue + approvals + KPIs\nmeter + budget + kill-switch + audit"]
    end

    subgraph Care["Customer care & martech"]
        TICKET["trouble-ticket TMF621"]
        INTER["party-interaction TMF683"]
        COMM["communication TMF681"]
        CAMP["campaign (martech)\nevent-triggered journeys · A/B · lift"]
    end

    WHOLESALE(["Wholesale access OWNER\n(third-party fibre — MEF LSO Sonata)\nseeker: a mock/real OSS · provider:\nANOTHER tenant on this platform"])

    FLOW["flow — Live Flow\n(read-only event observability)"]
    KAFKA[("Kafka\nbss.*.events\ntransactional outbox")]
    IDP[("OIDC IdPs\none issuer per tenant\n(Keycloak realms in dev)")]
    LEGACY[("A wrapped LEGACY BSS\n(per-tenant overlay seams —\nempty = native mode)")]
    PG[("PostgreSQL\nDB per component\ntenant_id + RLS")]

    SHOP & BIZ & CSR & ADMIN & APP & DEALER & PARTNER --> GW
    AGENTS -->|"/acp/* — per-tenant gate\noff → 404 · discovery → feed only"| GW
    CRAWLERS -->|"DUAL-SERVE by User-Agent:\nbot HTML + JSON-LD from TMF620\nrobots.txt per ai-visibility switch"| GW
    WORKERS -->|"digital-worker badge (revocable)\nworkforce queue + TMF doors;\nrefunds/cease only as approvals"| GW
    GW -->|"/workforce-runtime/*\n(caller staff-verified by the BSS)"| WCTL
    WCTL -.->|"hire = start · fire = stop\nrolls workers on worker-ai-* change\nsurge scaling ≤ governance maxWorkers"| WORKERS
    GW --> Party & Core & Revenue & Decisioning & Care

    ORD -.->|"machine calls\n(acting tenant's identity)"| CAT & PARTY & INV & STOCK & CHOICE & PAY & AGR & PROMO
    CART -.->|"ACP complete: pricing from the feed,\norder + payment on the CALLER'S\ndelegated token (RFC 8693)"| CAT & ORD & PAY
    ORD -.->|policy check / price| POLICY
    BILL -.-> INV & CAT & PAY & USAGE & PROMO & POLICY
    PAY -.-> VAULT
    REC -.-> CAT & INV
    QUOTE -.-> CAT & AI
    SOM -.-> PORT
    SOM -.->|"wholesale access-SEEKER: buys the\nL2/L3 input a fiber component reliesOn,\nover a Sonata adapter (async callback)"| WHOLESALE
    WHOLESALE -.->|"PROVIDER side (a sibling tenant):\ninbound Sonata serviceOrder →\nactivate → notify retailer → settle"| GW
    QUAL -.->|"multi-owner coverage: which\nowner serves this address at L2/L3"| WHOLESALE
    DEVCOM -.->|"financing port: internal instalments ·\nmock-bank loan · BNPL via the PSP rail"| PAY
    MIGRATE -.->|"one notice-gated TMF622\nmodify per subscriber"| ORD
    BILL -.->|"collections enforcement:\nrestrict (emergency whitelist) ·\nnonpayment suspend"| SOM
    CAMP -.->|"delivers via\nmachine identity"| COMM
    SHOP & APP -.->|"consent + beacons\n· For-you rail"| INSIGHT & AI
    INSIGHT -.->|experience rules| POLICY
    AI -.->|"campaign copy · product copilot\n· advisor · caption (governed)"| CAMP & CAT & KNOW
    CSR -.->|"copilot: 360 summary,\nNBO, ticket reply"| AI
    AI -.->|"churn engine → ChurnRiskDetectedEvent"| KAFKA
    AI -.->|"workforce queue DERIVES from\nreal backlogs (never copied)"| TICKET & BILL
    ROLES -.-> IDP
    CAT -.->|"federation: read-through,\nlegacy- prefixed, fail-soft"| LEGACY
    ORD -.->|"fulfilment HAND-OFF for\nlegacy- items (fail-soft, logged)"| LEGACY
    AI -.->|"workforce works the legacy\nincident backlog — completion\nVERIFIED against legacy state"| LEGACY

    Core & Revenue & Care & Decisioning -->|events| KAFKA
    KAFKA -->|"tenant-tagged envelopes"| COMM & INTER & FLOW
    KAFKA -->|"ProductOrderCreateEvent"| SOM
    SOM -.->|"completes digital orders
(acting tenant's identity)"| ORD
    Party & Core & Revenue & Care & Decisioning --- PG
```

## 2. Tenancy view (pool model with two locks)

Tenant identity derives from the **verified token issuer** — never from a claim a user could
edit. Anonymous traffic gets its tenant from the hostname at the gateway. Data isolation is
enforced twice: every query carries the tenant predicate in code, and PostgreSQL Row-Level
Security makes even predicate-free SQL tenant-safe.

```mermaid
flowchart LR
    subgraph GenAlpha["Tenant: genalpha"]
        GA_USER["customer/staff\nissuer: /realms/bss"]
        GA_SHOP["localhost → genalpha"]
    end
    subgraph Nova["Tenant: nova"]
        NV_USER["customer/staff\nissuer: /realms/nova"]
        NV_SHOP["*.nova.localhost → nova"]
    end

    GA_USER & NV_USER -->|"JWT (issuer verified\nper-tenant JWKS)"| SVC
    GA_SHOP & NV_SHOP -->|"anonymous + X-Tenant-Id\n(stamped by gateway,\ninbound copies stripped)"| SVC

    SVC["Any component\nTenantScope: issuer ▸ header ▸ default\nMachine calls: acting tenant's client credentials"]

    SVC --> RLS["PostgreSQL\nrestricted role per service\nSET app.tenant_id per connection\nPOLICY: tenant_id = current_setting\n(no tenant ⇒ zero rows; '__system__' for sweepers)"]
```

Cross-tenant access reads as **404, never 403** — foreign ids do not leak existence. The same
pattern stacks three deep: tenant (operator) → org (partner/business unit, via the `org` claim)
→ party (customer).

## 3. Order-to-bill sequence

The storefront journey exercises almost every component. Staff completion is the current
BSS→SOM handoff seam (a thin service-orchestration layer can replace the manual step without
changing anything else).

```mermaid
sequenceDiagram
    autonumber
    actor C as Customer (browser)
    participant GW as gateway
    participant ADDR as address 673
    participant QUAL as qualification 679
    participant CART as cart 663
    participant ORD as ordering 622
    participant STOCK as stock 687
    participant PAY as payment 676
    participant VAULT as vault 670
    participant INV as inventory 637
    participant AGR as agreement 651
    participant PROMO as promotion 671
    participant BILL as billing 678
    participant USE as usage 635/677
    participant COMM as communication 681

    C->>CART: build cart (guest ok, promo code validated anonymously)
    C->>ADDR: validate address → standardized form
    C->>QUAL: serviceability check (gated offerings)
    C->>PAY: authorize one-time charges (card / PSP mock)
    opt save this card
        C->>VAULT: store token + last4 (never the PAN)
    end
    C->>ORD: productOrder (items, promo code, payment ref)
    ORD->>STOCK: reserve devices

    Note over ORD: completion — the SOM auto-completes digital orders<br/>(via ProductOrderCreateEvent) — staff/fulfilment completes physical ones
    ORD->>INV: provision products per item
    ORD->>STOCK: consume reservation
    ORD->>PAY: capture payment
    ORD->>AGR: mint commitment agreements (offering terms)
    ORD->>PROMO: redeem the promo for the owner

    Note over BILL: monthly billing run (staff/scheduled)
    BILL->>INV: active products per owner
    BILL->>USE: rate the period's usage → overage charges
    BILL->>PROMO: redemptions → discount lines
    BILL->>PAY: settle via new card or vaulted method

    ORD--)COMM: events → "Order received / complete"
    BILL--)COMM: events → "Your bill is ready"
    COMM--)C: tenant-scoped inbox notifications
```

## 4. Event backbone

Every write that matters is captured in the same transaction as the business change
(**transactional outbox**), relayed to Kafka, and consumed idempotently. Envelopes carry the
tenant, so downstream consumers stay partitioned without knowing anything about tenancy rules.

```mermaid
flowchart LR
    SVC["component tx:\nbusiness change + outbox row\n(same commit)"] --> RELAY["outbox relay\n(every 2s)"]
    RELAY --> K[("bss.&lt;component&gt;.events\n{eventId, eventTime, eventType,\ntenantId, event{...}}")]
    K --> COMM["communication TMF681\nidempotent per (tenant, eventId)\nacts as the envelope's tenant"]
    K --> CAMP["campaign engine\nmatch trigger -> once per customer\nacts as the envelope's tenant"]
    CAMP -->|"TMF681 send\n(acting tenant's machine identity)"| COMM
    K --> FUT["future consumers:\nanalytics, churn scoring"]
    COMM --> INBOX["customer inbox\n(party- and tenant-scoped)"]
```

Editorially mapped today: order received/completed, bill ready, ticket resolved, installer
booked, cart abandoned ("still thinking it over?"). The campaign engine consumes the same
stream: a campaign is a trigger (event type, optionally a state) plus a message template and
an optional promotion code — matched campaigns reach each customer **exactly once** (a unique
execution row per tenant/campaign/party is the guarantee), delivered as TMF681 messages under
the acting tenant's machine identity.

## Boundary notes

- **Commerce seams are bring-your-own, per tenant.** Three provider families follow one config
  shape — a row-level-scoped config table storing a `secret_ref` (an ENV-VAR name, never the
  key), a runtime registry of named adapters, and a global-env fallback so unbound deployments
  never change behaviour: **content** (Sanity by name or any headless CMS via the generic HTTP
  connector; reference-mode — the shop 302s to the operator's own CDN), **carriers** (the
  delivery menu — Helthjem/Posten-Bring/PostNord adapters, pickup points, postcode routing;
  the shopper's pick rides the order's delivery place to the booking), and **payments** — with
  one deliberate exception: **money has no generic connector.** PSPs are named adapters only;
  redirect/BNPL providers (Klarna, Vipps MobilePay, PayPal) confirm a session idempotently across the return leg
  AND an HMAC-verified webhook; card charges route by currency/priority and fail over only on
  connect-level outages (a decline or an ambiguous timeout never retries) under one idempotency
  key; and the revenue subledger books a BNPL capture as a provider **receivable** (1100) that
  the payout's remittance clears to cash — the books say when the merchant was actually paid.
- **The agent channel is gated, not assumed.** AI shopping agents reach only `/acp/*` (the
  product feed and the checkout-session lifecycle), behind the gateway's per-tenant
  `agent-commerce` switch: `off` → 404 (dark), `discovery` → feed only, `full` → delegated
  checkout. Newborn tenants are born `off`. Checkout never uses a machine identity — the
  shopper's token is exchanged (RFC 8693, Keycloak standard token exchange) through the
  `bss-agent` client into a credential scoped to exactly catalog-read + order + pay; the cart
  service forwards that token downstream and never lends its own.
- **The digital workforce is employed, not installed.** A worker (Hermes or any MCP runtime)
  holds a revocable `digital-worker` staff badge minted on TMF672 — the grant sheds the walk-in
  customer defaults, the revoke is the firing. Its task queue on the intelligence component
  derives LIVE from real backlogs (unassigned tickets, unapplied cash); claims are leases;
  completion is VERIFIED against the source system; refunds/cease/erasure only ever become
  approval rows a human executes with their own token; the tenant's one AI kill-switch stops
  workers and copilots alike; and the console's Workforce tab is the scoreboard (deflection,
  handle time, reopen rate, minutes-saved labeled as the estimate it is).
- **A legacy estate is just another seam (proven, suite #67).** Three per-tenant registry
  fields — `legacy-catalog-base-url`, `legacy-fulfilment-base-url`, `legacy-ticket-base-url`
  (empty = native mode) — wrap an existing BSS: its catalog federates read-through into the
  native list and the ACP feed (legacy-prefixed, cached, fail-soft — a dead legacy never breaks
  the native catalog); orders carrying legacy- items hand off to the legacy work-order queue
  (genalpha keeps the engagement record — never two writers); and the digital workforce works
  the legacy incident backlog age-stamped, with completion verified against the LEGACY system's
  own state.
- **Crawlers get a third face of the same catalog (suite #68).** AI answer-engine bots execute
  no JavaScript, so the gateway dual-serves `/shop/offering/*` by User-Agent: crawlers receive
  complete HTML with schema.org Product/Offer JSON-LD rendered LIVE from TMF620 (the suite
  proves bot price == catalog price — the faces are proven equal, never maintained equal);
  humans keep the untouched SPA. The per-tenant `ai-visibility` switch (open | search-ai |
  search-only | dark) executes at robots.txt — the lever crawlers actually obey. The roster of
  crawlers is configuration tagged **retrieval** or **training** (`bss.geo.crawlers`), so
  `search-ai` says the thing operators actually want — AI search yes, model training no —
  which `search-only` could not say while it blocked GPTBot and OAI-SearchBot in one list
  (suite #246). `dark` also answers `X-Robots-Tag: noindex`, because a Disallow stops a fetch
  and leaves a bare URL indexable. Newborn tenants default search-only; llms.txt ships where
  AI answer engines are welcome, honestly labeled speculative.
- **The loop closes only by opt-in.** The workforce package's `worker-controller` is the sole
  holder of container spawn-rights (docker.sock / a scoped ServiceAccount — deployed via
  `--profile workforce`, never by default): one dashboard click hires a RUNNING worker with the
  badge injected (credentials touch no human), one click fires it (badge revoked AND container
  stopped), and the worker's LLM (`worker-ai-provider/base-url/api-key/model` in tenants.yml,
  live-refreshed) hot-swaps by config — the controller rolls the containers, safely, because
  claims are self-expiring leases. Suite #66 proves all of it, including the live brain swap.
- **The Production seam is now real (thin).** service-orchestration consumes order events,
  decomposes digital orders into TMF641 service orders, mock-activates (TMF640's stand-in),
  records TMF638 services and completes the product order machine-side. Physical/install
  orders still complete on fulfilment. Assurance is live in the same thin spirit: TMF642
  alarm intake (a simulator in dev), critical alarms auto-minting one open TMF656 service
  problem per affected object, resolution clearing the alarms — and agents see open outages
  as a banner across the CSR console.
- **A bundle fulfils as the several things it is.** A triple-play order decomposes into
  per-component leaf items (Internet / TV / Mobile / device) carrying `orderItemRelationship:
  reliesOn` — TV waits for the broadband it rides; a physical-SIM line stays *reserved* until the
  SIM lands; each leaf runs on its own clock and the order rolls up `partiallyCompleted`, never
  one opaque state. Fixed components are flagged so they bill **through** the bundle (billing is
  byte-identical — decomposition changed the *tracking*, not the money), and a same-offering
  characteristic change (a broadband speed tier) is an in-place TMF622 `modify`, gated: numeric
  downgrades are refused while a commitment is live, off-menu values are refused by the spec's
  allowed set.
- **Wholesale / open-access is a product line, not a mode (honest boundary).** As an **access
  seeker** the orchestrator buys the L2/L3 access a fiber component `reliesOn` from an owner over
  a **MEF LSO Sonata** adapter — one impl per owner, async callback, with a bundled `mock-sonata`
  standing in for a real owner's OSS in dev (the real adapter is config, not code). As an **access
  provider** a *sibling tenant* runs the owner side: it accepts inbound Sonata `serviceOrder`s at
  `/mefApi/serviceOrdering/v1`, activates, notifies the retailer and settles per line, and the
  upstream COGS books to the revenue GL (DR wholesale-COGS / CR AP-wholesale). Seeker and provider
  meet **cross-tenant** — the one cross-tenant path in a system otherwise built to keep tenants
  apart, and it runs through the same gateway + X-Tenant-Id, never around RLS. **Mobile wholesale
  (MVNE)** is the usage-metered sibling: an MVNO is a tenant, so a second rating pass re-rates its
  own CDRs at a host's per-unit wholesale rate card into a per-period ledger (retail rating
  untouched), the settlement carries a reconciliation (rated vs live units), and revenue books the
  cost as COGS + a payable to the host (DR 5110 / CR 2110) — the fibre *seeker* settlement, by the
  megabyte instead of the line. The **provider face** completes it: the host bills *external* MVNOs
  (own-BSS) at a **per-MVNO rate card** (the SLA/tier lever), exposing a consolidated per-MVNO
  settlement + a per-MVNO statement their BSS pulls, and booking wholesale *revenue* (DR 1210 / CR
  4020) — so mobile, like fibre, supports **both** MVNO types. Both wholesale supply sides are
  authored from the console's **Wholesale desk** (owners, L2/L3 products realised by a **TMF633
  CFS/RFS** service catalog, coverage, and mobile rate cards), gated by `wholesale:admin`.
- **The browse cache is anonymous-only and route-scoped (staleness stated).** The gateway's
  `LocalResponseCache` caches only the catalog route's **token-absent** GETs (the catalog emits
  `Cache-Control: public, max-age=N` + `Vary: X-Tenant-Id`, and `no-store` the instant an
  `Authorization` header appears), so a logged-in cart/order/bill is never cached and one tenant's
  price list never serves another. Freshness is bounded by the short TTL, not actively invalidated
  — a price change propagates within `max-age` (seconds), the honest trade for absorbing a
  campaign-day surge before it reaches the JVM or Postgres. Stock ("only N left") and
  personalization are deliberately **never** cached.
- **Collections is orchestration over what the fleet already owns — shaped by statute.** Billing
  carries one `collectionCase` per financial account (a state machine `CURRENT → REMINDED → WARNED
  → RESTRICTED → SUSPENDED`, with `CURED` reachable from any state and promise-to-pay / dispute /
  hardship holds orthogonal to it) driven by a per-tenant `dunningPolicy` that sits **on top of a
  country statutory pack it cannot undercut** — the Norway pack pins the 14-day reminder-fee gate,
  the fee cap, the two-reminder cap, the one-month enforcement-notice clock and the minimum
  actionable floor, and a policy below the floor is refused at write time. Enforcement rides two
  SOM primitives: `restrict` (a new, lighter barring profile — outgoing barred, data throttled,
  **the emergency-call whitelist always on**) and nonpayment `suspend`, during which **billing
  skips MRC accrual** (the statutory rule that no subscription charges bill through a nonpayment
  block). Cure auto-resumes with a configurable (zeroable) reconnection fee. Faces: a console
  Collections desk where the statutory floor renders read-only — the law's numbers are never form
  fields — and a storefront outstanding banner with customer-initiated promise-to-pay
  (`collections_test.js`).
- **Device commerce is a component, not a checkout feature (:8145).** `device_agreement` binds
  party + subscription + IMEI to financing terms under **one `FinancingProvider` port with three
  drivers** — internal (operator-book, delegating to billing's instalment machinery), mock-bank
  (third-party loan: the financier owns title and receivable; upgrades fetch an early-settlement
  quote) and BNPL through the existing payment-rail adapter (settlement delegates to the
  provider) — so a tenant picks a financing model as config, and total cost of ownership is a
  required, displayed field on every offer face. Trade-in is a state machine (quote → accept →
  transit → grading → delta the customer accepts), the 14-day withdrawal clock starts at the
  **parcel-delivery event** the bundle arc already emits, and lost/stolen pushes the IMEI through
  a blacklist adapter seam (mock driver; a real EIR feed is deployment config — a blacklisted
  IMEI quotes zero). Revenue consumes the agreement events into an **IFRS 15 subsidy subledger**:
  contract asset at activation, monthly unwind, ETF recovery, swap write-off vs trade-in
  inventory, residual-value-guarantee accrual (`device_commerce_test.js`).
- **Base migration executes what the simulator only predicted (:8146) — and the gates are
  server-side.** A `migrationPlan` (source→target matrix, per-row beneficial/neutral/detrimental
  delta class) **cannot arm without an attached price-simulation** — rehearse-first is a 409, not
  a doctrine slide. Each customer walks `scheduled → noticed → exit-window → order-emitted`, with
  `orderDate ≥ noticeSentAt + noticeDays` enforced as a gate (30 days in the EEA jurisdiction
  pack), then the engine emits **one TMF622 `modify` order per subscriber** — the existing
  proration-safe plan-change path, untouched. A detrimental delta exposes the penalty-free exit
  the European telecom code grants (in-binding included); age rules run auto-migrate or
  lapse-to-grandfather; promo expiry gets a courtesy notice and no exit right (disclosed at
  sale). Snapshots make rollback an inverse order; a consecutive-failure circuit breaker pauses
  the plan and keeps the evidence (`base_migration_test.js`).
- **Usage policy is one monetary-meter primitive wearing three legal faces.** A `spend_meter`
  accumulates rated charges per subscription per cycle under a threshold policy; its three
  instances are the customer-settable subscription cap, the **content-services cap** (barring
  free and always available, the lowest selectable limit floored by statute, block-on-breach
  with notification, mandatory barring categories for minors) and the **roaming financial
  limit** (≈€50 ex-VAT default per the EEA roaming regulation, warning at 80%, hard cut-off at
  100%, the explicit continue an audited event). Household **allowance pools** decrement through
  small reserve-then-commit grants — never the whole remainder to one session — so a concurrent
  burst cannot over-allocate the pool; per-member caps fall back to own allowance. Auto top-up
  is opt-in with recorded consent and per-cycle caps, idempotent per breach window. Travel
  passes are time-boxed, zone-filtered boosts; the first record from a new zone emits a
  zone-entry event carrying the mandatory pricing information. **Zero-rating is deliberately
  absent** — per-app price differentiation is unlawful in the EEA, so free-data promos are
  application-agnostic bonus meters; the RLAH fair-use monitor is a named follow-up
  (`usage_policy_test.js`).
- **The reference geography's rails are adapters behind generic ports — never hard
  dependencies.** Bill distribution is an ordered channel chain per party (`e-invoice alias →
  digital mailbox → print`), each send recording the channel actually used; the direct-debit
  loop (mandate file in → per-cycle claim out → OCR settlement back) closes bills **through the
  existing remittance door**, and three rail mocks (:8147/:8148/:8149) keep every leg provable
  offline. Registry sync polls the national event feed off a stored person-id link (idempotent
  cursor), re-fetches on address/name/death events, and treats a **protected address** as a
  first-class flag: no street stored or shown anywhere — staff consoles included — checkout
  falls to pickup-point delivery, and every export honors it. The directory obligation is
  modeled before any export exists (exposure levels, free secret numbers, a delta exporter that
  can never leak a reserved entry). The credit-decision port slots into the existing TMF696
  risk signal; a bureau freeze returns a distinct state so checkout offers prepaid, and only
  the decision is stored. Deferred honestly: the production eID-broker federation (the step-up
  gate exists; a mock broker container is a follow-up), real directory/bureau/rail contracts —
  per-tenant deployment concerns behind ports (`norway_rails_test.js`).
- **Composability is real**: cross-component calls go through conditional clients with Noop
  fallbacks, channels hide features whose component is absent, and Helm skips disabled modules
  entirely — see the [composer](composer.html).

**The charging seam is a real OCS, routed per tenant.** `service-orchestration`
(lifecycle: provision / plan change / hold / transfer) and `usage` (the TMF654
balance face + the running-low door) each resolve the tenant's Online Charging
System from `tenants.yml` (`ocs-provider`, `ocs-base-url`, credentials) through
a registry of named adapters behind one router — the same shape as the CMS
seam. `http` speaks the subscriber/rate-plan REST shape of `mock-ocs` (a vendor
gateway in production); `sigscale` drives **SigScale OCS**, an open-source
(Apache-2.0) 3GPP Diameter Ro/Gy OCS bundled in the fleet
(`integrations/sigscale-ocs`, port 8155 REST / 3868 Diameter) over its native
TM Forum APIs — offerings are rate plans, products are subscriptions, services
carry the MSISDN the packet core charges against, its TMF654 balance hub posts
the OCS-owned "running low" line back to `usage`. The BSS is never in the
charging path; it references charging from the catalog (`chargingSpecId`) and
provisions it, fail-open. Suite #123 `sigscale_ocs_test` drives a real Gy
credit-control session. Detail: `docs/ocs-seam.md`.

**The phone asks the BSS what it may use.** `device-entitlement` (`:8156`) is a
GSMA TS.43 Entitlement Configuration Server as an ODA component. Its device door
(`/ts43`, through the gateway, anonymous at HTTP level) authenticates the SIM
with EAP-AKA relayed over HTTP against the **AUC seam** (`AucClient` → the
operator's HSS/UDM adapter; `mock-hss` in dev) and answers per TS.43 app id —
VoLTE/VoNR, Wi-Fi calling (with the emergency-address service flow), SMS over
IP, data plan, and eSIM ODSA for companion and primary devices — from the plan's
product specification and the line's state (followed from `bss.som.events`).
Its BSS face (`/tmf-api/deviceEntitlement/v1`) binds IMSIs to lines and explains
entitlements in words. Events: `bss.entitlement.events`. Suite #124
`device_entitlement_test` drives a real TS.43 client (`mock-ts43-device`,
Milenage USIM) through the relay. Detail: `docs/entitlement-seam.md`.

**ontology** (`:8160`, `/ontology/v1`) — the Operational Semantic Registry: the
GenAlpha Operational Ontology as a component. A YAML registry in the repo
(`ontology/`: concepts with SID/TMF/ODA lineage, governed actions, typed
capabilities, component descriptors, per-tenant overlays) is schema-validated
and reference-checked at startup and served merged for the caller's tenant.
`check` evaluates an action's typed preconditions, permissions and policy domain
and names every verdict in words; `execute` runs the mapped TM Forum capability
**with the caller's own token and channel** and writes a decision receipt
(`DecisionRecordedEvent` on `bss.ontology.events`, ingested by insight's decision
log). The same definitions generate the Ontology MCP tools (`POST /ontology/v1/mcp`)
and the TypeScript SDK (`packages/genalpha-sdk`), explain every console page to
the ? drawer, and describe the component at `/.well-known/genalpha-component.json`.
Stateless: no database, a machine identity for the policy pre-check only. Suite
#125 `ontology_test` proves conformance (gateway routes, event sources, realm
roles, self-description, SDK diff) and the first journey, `upgradeSubscription`,
end to end with its receipt — and, from the same day's second arc, nine more governed
actions (pause, resume, SIM, cancel, dispute, credit with a maker-checker ladder that files
above-threshold requests on the workforce approval desk, and launch request/approve/hold),
runtime self-description on the six journey components with `GET /ontology/v1/conformance`,
an outcome sweep that judges receipts (retained / changed / lost), learning contracts on
`ontology.*` decision points, a customer context resolver, an RDF/Turtle export, and the CSR
desk running on the generated SDK. Detail: `docs/ontology-seam.md`; rationale:
`docs/ontology-research.md`.

## 5. Cloud deployment view — proven on both AWS and Azure

The same Helm chart deploys the whole fleet; only the *substrate* differs, and it slots in
behind seams the application never sees. Both stacks ran live (single-node soak scope: core
commerce + billing×2 + the consoles), each proving the P0 tick-locks under two billing
replicas against a **managed** database.

```
        laptop (build) ──push──▶ registry ──pull──▶ managed k8s ──▶ managed Postgres
  AWS:   docker images           ECR                 EKS (Graviton)    RDS (single-AZ)
  Azure: docker images           ACR                 AKS (x86)         Flexible Server
```

What stays identical across clouds: the chart, the images, the in-cluster Kafka + Keycloak,
the seed realms, the smoke (`ops/k8s-soak/smoke.js`), and the security model. What the seams
absorb: the database is `local.postgres.enabled=false` + a managed host; the registry is one
`image.prefix`; the ingress/port-forward story is unchanged.

### Differences observed between the two clouds (live-run truths)

| Dimension | AWS EKS | Azure AKS |
|---|---|---|
| **Cluster access** | EKS module v20 grants the creator NO access by default — needs `enable_cluster_creator_admin_permissions` | AKS grants the creating principal admin automatically |
| **Node architecture** | Graviton (`t4g`, ARM) native + ~30% cheaper — matched the arm64 images directly | Sponsorship tier offered **no ARM sizes at all**; forced x86 (`EC2as_v5`) + cross-building images amd64 with buildx |
| **VM availability** | One instance family, available on ask | Gated **twice** — the offered catalog (`az vm list-skus`) AND per-family vCPU quota (`az vm list-usage`); a size can be listed with zero quota |
| **Region eligibility** | Any region in the account | New subscriptions **refused** westeurope outright; swedencentral had no AKS capacity — landed in northeurope |
| **Registry auth** | `aws ecr get-login-password` per session | Managed-identity `AcrPull` role — nodes pull with no password |
| **Database connections** | RDS `db.t4g.small` ≈ ample for 13 pools of 5 | Flexible Server `B1ms` caps ~50 — the fleet exhausted it; raised `max_connections` + shrank Hikari idle pools to 1 |
| **Postgres extensions** | pg_trgm / vector available on the image | Flexible Server refuses `CREATE EXTENSION` unless allow-listed via `azure.extensions` first |
| **TLS** | RDS accepts plaintext in-VPC | Flexible Server demands TLS by default — turned off for the soak (production keeps it, adds `sslmode=require`) |
| **Database creation** | psql init job over the fleet's `init-databases.sql` | 28 databases as Terraform resources — no init step |
| **Provisioning time** | ~20 min (EKS control plane dominates) | ~8 min cluster, but the sponsorship-tier gauntlet made the *session* longer |

The through-line: **the application code never changed** — every difference lived in Terraform
and two Helm `--set`s. The tenancy model, the RLS second lock, the tick-locks, the outbox, the
GDPR endpoints all behaved identically on both clouds. "Any cloud" is now two invoices, not a
claim. The AKS run also hardened the chart for *any* managed Postgres (bounded idle pools) and
gave the storefront a host-prebuilt image path (`Dockerfile.prebuilt`) for when
vite-under-emulation misbehaves.
