# Versioned financial-policy contract hardening

Use this pattern for sensitive configuration objects such as portfolio policies, allocation mandates, approval rules, or other immutable financial control records.

## Preview and confirmation identity

- Preview is pure/read-only: it must not write rows, audits, timestamps, caches, or request reservations.
- Return two server-generated values:
  - `preview_id`: deterministic hash-derived identifier binding the exact canonical payload;
  - `confirmation_id`: unique identifier binding one confirmation request.
- Confirm requires the exact payload, `preview_id`, `confirmation_id`, and explicit `confirm=true`.
- Same confirmation ID + same canonical payload hash returns the original result idempotently.
- Same confirmation ID + changed preview or payload fails closed.
- A new confirmation ID may intentionally confirm identical content as a new version. Test A → B → A as versions 1, 2, 3, with version 3 pointing to version 2.
- Do not use a permanent unique content fingerprint as idempotency identity; that incorrectly resurrects an older version and blocks deliberate re-confirmation.

## Atomic storage and compatibility

- Use a write transaction that serializes version allocation and active-version changes (`BEGIN IMMEDIATE` for SQLite).
- In one transaction: find current active version, allocate next version, deactivate previous, create allowlisted audit, insert policy and allocations, commit.
- Enforce one active version and unique non-null confirmation IDs with indexes.
- Preserve old rows during additive migration: nullable `confirmation_id`/`payload_hash` for legacy records; new records populate both.
- Keep request identity immutable with an additive trigger.
- In the `IntegrityError` race path: rollback, re-read by confirmation ID, compare payload hash, then return idempotently or reject conflict. Never fall back to a content fingerprint.
- Test migration both on an empty database and a synthetic prior-version schema containing an existing row.

## Minimal audit

For policy confirmation, store the policy ID as `entity_id`. Audit detail should be an explicit allowlist, e.g. exactly:

```json
{"version": 3, "previous_policy_id": "policy-…"}
```

Never audit money, percentages, bands, allocations, objectives, benchmarks, restrictions, payloads, hashes, preview IDs, or confirmation IDs. Parse the audit JSON in tests and compare its key set exactly; substring checks are insufficient.

## Decimal and null semantics

- Accept exact decimal text and calculate with `Decimal`; reject binary floats at service boundaries.
- Preserve missing values as `null`, never implicit zero.
- Money: null or finite non-negative Decimal.
- Percentages: null where optional, otherwise finite Decimal in `[0, 100]`.
- Allocation targets total exactly 100; enforce lower ≤ target ≤ upper and crypto target ≤ crypto cap.
- Unsupported benchmark weights fail closed with a field-specific German error rather than being ignored.

## Historical read-only detail

- Add one GET within the existing API convention; return active or inactive immutable versions and only their own allocations.
- Unknown opaque ID returns a generic 404 that does not echo the ID.
- OpenAPI should expose GET only; no reactivate/copy/edit endpoint.
- Prove repeated GET leaves policy, allocation, audit, timestamps, and `total_changes` untouched.
- In UI, render a clearly marked `Nur lesen · Schreibgeschützt` detail without save, activate, copy, confirm, recommendation, rebalance, or order actions.

## Frontend stale-preview defense

- Store a deep copy of the displayed payload alongside both server identifiers.
- Any edit after preview invalidates preview state and closes confirmation UI.
- Ignore a preview response if the form changed while the request was in flight.
- Immediately before confirm, compare current form to the stored preview payload.
- Forward both identifiers unchanged; never generate or log them client-side.
- Set a synchronous `confirming` guard and close/disable confirmation UI before awaiting the request, preventing double-click duplicates.
- Map validation, conflict, 404, and general failures to concise German messages without IDs or payloads.

## Verification focus

Cover read-only preview, request identity conflicts, same-content new versions, A → B → A, predecessor links, exactly one active row, immutable older rows, exact audit allowlist, Decimal/null validation, migration compatibility, forced post-IntegrityError recheck, GET non-mutation, OpenAPI/write security, stale-preview invalidation, and duplicate-submit suppression before full suites and UAT.
