target-revenue/migrations/0002_ledger.sql
tegwick 5064815e77 Implement hosted Target Ledger append API (WP-0006-T04)
migrations/0002_ledger.sql adds ledger_entries with an identity-column
sequence for exact append order and no UPDATE/DELETE grant for trf_app.
src/target_revenue/ledger.py: append_entry() rejects caller-supplied
previous_entry_hash/signature, enforces per-Licensor phase ownership,
serializes concurrent appends via pg_advisory_xact_lock, computes the
chain tip and signs with the Trust Service instance's own Ed25519 key
(service/keys.py), reusing validation.py's checks unchanged. Adds
POST/GET /phases/{id}/ledger and an unauthenticated GET /public-key.

Also fixes a route-ordering bug found while wiring this in: phase IDs
never needed the {phase_id:path} converter (they contain colons, not
slashes), and its greedy matching was swallowing /ledger-suffixed
paths into the plain GET /phases/{id} route.

tests/test_ledger_hosting.py (8 Docker-gated tests) exercises hash-chain
linkage, forged-field rejection, cross-Licensor isolation, currency and
duplicate-id rejection, DB-privilege enforcement, signature
verification via the public-key endpoint, and the task's own
highest-priority property: append -> export -> offline fold reproduces
the exact expected Development/Remission Credit and Outstanding Target.
2026-07-29 21:34:27 +02:00

33 lines
1.4 KiB
PL/PgSQL

-- WP-0006-T04: hosted Target Ledger append API.
-- Depends on migrations/0001_registries.sql (phase_manifests, licensors).
BEGIN;
CREATE TABLE IF NOT EXISTS ledger_entries (
sequence bigint GENERATED ALWAYS AS IDENTITY,
entry_id text NOT NULL,
phase_id text NOT NULL REFERENCES phase_manifests(phase_id),
entry jsonb NOT NULL,
previous_entry_hash text NOT NULL,
signature text NOT NULL,
recognized_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (phase_id, sequence),
UNIQUE (entry_id)
);
-- Ordering within a Phase must be exactly append order for the fold and
-- hash chain to mean anything; index supports the ORDER BY sequence read
-- path (registry/ledger.py get_ledger()).
CREATE INDEX IF NOT EXISTS ledger_entries_phase_sequence_idx
ON ledger_entries (phase_id, sequence);
GRANT SELECT, INSERT ON ledger_entries TO trf_app;
-- Deliberately no UPDATE, no DELETE, and no direct control over `sequence`
-- (GENERATED ALWAYS AS IDENTITY — trf_app cannot even attempt to set it) for
-- trf_app: append-only is a database fact here, matching phase_manifests
-- and extensions in migrations/0001_registries.sql, not merely an API
-- design intention (ADR-0002 compensating guardrail 2).
GRANT USAGE, SELECT ON ledger_entries_sequence_seq TO trf_app;
COMMIT;