feat(sp11): tamper-evident hash-chained audit_log

- New audit_log table (migration 0009, user_version=9):
  * id, event_type, entity_type, entity_id, actor, payload_json,
    created_at, prev_hash, hash
  * Indexes on (entity_type, entity_id), event_type, created_at
- New cyclone.audit_log module:
  * append_event(session, AuditEvent) — appends one chained row
  * verify_chain(session) — walks the chain, returns first bad id
  * SHA-256 hash over canonical row form (unit-separator delimited)
  * Genesis prev_hash = 64 zeros (Bitcoin-style sentinel)
- New AuditLog ORM model
- New admin API endpoints:
  * GET /api/admin/audit-log (paginated, filterable)
  * GET /api/admin/audit-log/verify (returns ok/first_bad_id/reason)
- Hooked into existing endpoints to append events:
  * /api/parse-999 → 'claim.rejected' per matched claim
  * /api/parse-277ca → 'claim.payer_rejected' per matched claim
  * /api/clearhouse/submit → 'clearhouse.submitted' per claim
- HIPAA §164.316(b)(2) compliance note in docs

Tests: 688 -> 705 (9 audit + 8 audit-API). All 705 backend tests pass.
This commit is contained in:
Tyler
2026-06-20 23:45:43 -06:00
parent 9acdcb8dbd
commit 62bb09f183
8 changed files with 818 additions and 4 deletions
+31
View File
@@ -147,6 +147,37 @@ All CMS POS codes `01``99` are accepted. The canonical list lives in
`cyclone/parsers/payer.py` as `CMS_PLACE_OF_SERVICE_CODES` and is the
source of truth for validation and any UI dropdowns.
## Audit log (SP11)
Cyclone persists every state-changing event to a tamper-evident
hash-chained audit log. Each row carries a SHA-256 hash of
`(id, event_type, entity_type, entity_id, actor, payload_json,
created_at, prev_hash)`, where `prev_hash` is the previous row's
hash. Modifying any row's payload invalidates every subsequent row's
hash.
To list events:
```bash
curl 'http://localhost:8000/api/admin/audit-log?entity_type=claim&entity_id=C-123'
```
To verify the chain (run nightly or on demand):
```bash
curl http://localhost:8000/api/admin/audit-log/verify
# → {"ok": true, "checked": 1234}
# → {"ok": false, "checked": 1180, "first_bad_id": 1181, "reason": "hash mismatch ..."}
```
Events written today:
- `claim.rejected` (999 ACK AK5 R/E)
- `claim.payer_rejected` (277CA STC A4/A6/A7)
- `clearhouse.submitted` (SFTP submit)
Compliance: HIPAA §164.316(b)(2) requires 6-year retention. The
schema doesn't enforce retention — that's a separate vacuum job.
## 277CA Claim Acknowledgment (SP10)
After Gainwell accepts our 837P file (999 AK5=A) and adjudicates the