/** * T3 — account registry (immutable, INV8). `createAccount` writes a version-1 snapshot; * `setAccountStatus` performs the atomic append-version + pointer-swap (prior snapshot stays * readable from `versions()`). Never mutates a lifecycle field in place. */ import type { AccountRecord, AccountStatus } from '../model/records.js' import type { AccountStore } from '../store/ports.js' import type { AuditWriter } from '../audit/log.js' import { noopAuditWriter } from '../audit/log.js' import type { PlanTier } from 'relay-contracts' import { newUuid, nowIso } from '../util/ids.js' export interface AccountRegistry { createAccount(plan: PlanTier): Promise getAccount(accountId: string): Promise setAccountStatus(accountId: string, status: AccountStatus): Promise } export interface AccountRegistryDeps { readonly accounts: AccountStore readonly audit?: AuditWriter readonly actor?: string } export function createAccountRegistry(deps: AccountRegistryDeps): AccountRegistry { const audit = deps.audit ?? noopAuditWriter() const actor = deps.actor ?? 'system' return { async createAccount(plan) { const rec: AccountRecord = { accountId: newUuid(), // unguessable, never recycled (INV1 precondition) plan, createdAt: nowIso(), status: 'active', } await deps.accounts.insert(rec) await audit.writeAuditEvent({ action: 'account.create', principalId: actor, accountId: rec.accountId, hostId: null, ts: rec.createdAt, meta: { plan }, }) return rec }, async getAccount(accountId) { return deps.accounts.get(accountId) }, async setAccountStatus(accountId, status) { const next = await deps.accounts.swapStatus(accountId, status, actor) // NEW snapshot, atomic swap await audit.writeAuditEvent({ action: status === 'suspended' ? 'account.suspend' : 'manage', principalId: actor, accountId, hostId: null, ts: nowIso(), meta: { status }, }) return next }, } }