# Clinic CMS — audit checklist

The master list. Each row says what was actually done to reach its verdict, so a
`PASS` can be argued with rather than taken on trust.

Statuses: **PASS** (checked, sound) · **FIXED** (defect found and repaired)
· **PARTIAL** (checked, some of it) · **FAIL** (broken, not repaired)
· **UNVERIFIED** (not established).

Findings in full: [CMS-AUDIT-PROGRESS.md](CMS-AUDIT-PROGRESS.md).
Inventory: [CMS-AUDIT.md](CMS-AUDIT.md).

---

## Architecture and data

| # | Area | Status | How |
|---|---|---|---|
| 1 | Layering / dependency direction | PASS | `deptrac` 0 violations; `ModuleBoundaryTest` |
| 1 | Business logic placement | PASS | Read every service in Billing, Laboratory, Pharmacy; controllers coordinate only |
| 1 | Dead code / wrong abstractions | FIXED | `ResultStatus::isSuperseded()` — uncalled and inverted; removed |
| 2 | Money representation | PASS | Every monetary column `BIGINT` minor units + currency; `Money` is the only arithmetic |
| 2 | Rounding / allocation | PASS | Read `Money::multiplyRatio` and `allocate`; exact-rational, largest-remainder, correct for negatives |
| 2 | Foreign keys / constraints | PASS | `RESTRICT` on history-bearing rows; unique idempotency per branch |
| 2 | Immutability of issued records | PASS | Versioning via `superseded_by_id`; `AuditLog` refuses update and delete |
| 2 | Public identifiers | PASS | ULIDs in URLs; no sequential ids exposed |
| 3 | Tenancy model | PASS | Physical: one clinic, one database. Verified there is no `clinic_id` and nothing assumes one |
| 3 | Branch scoping | PASS | Global scope + auto `branch_id`; `withoutBranchScope()` is explicit and greppable |

## Access control

| # | Area | Status | How |
|---|---|---|---|
| 4 | Route authentication | PASS | Module kernel loads `Routes/web.php` behind `['web','auth']`; only two public route files exist, both read |
| 4 | Authorization coverage | PASS | Scanned all 190 mutating actions; 9 unauthorized, all justified. Now held by `AuthorizationCoverageTest` |
| 4 | Suspended accounts | FIXED | `Gate::after` could not deny. Moved to `Gate::before` + `User::hasPermissionTo()` |
| 4 | Privilege escalation | PASS | 81 Auth tests, including role-grant and last-super-admin guards |
| 4 | IDOR on nested resources | PASS | Sampled invoice lines, prescription items, patient contacts and documents — all scoped to the parent by ULID |
| 4 | Self-scoped actions | PASS | Sessions and API tokens filter on the owning user in the service |
| 17 | Private file serving | FIXED | The whole PHI root had `GET`/`PUT` routes. Closed |
| 17 | Patient document download | PASS | Policy re-run per request, scoped to the patient, access logged |

## Clinical

| # | Area | Status | How |
|---|---|---|---|
| 7 | Note lifecycle | PASS | Draft → signed → amended/retracted; `scopeBindings()` on nested routes |
| 9 | Result lifecycle | FIXED | Retraction was a dead end — no replacement could ever be entered |
| 9 | Result versioning | FIXED | Replacement now supersedes the withdrawal, so exactly one row is current |
| 9 | Verification / second verifier | PASS | Read `verifyAll`; own work held, rest released; all-or-nothing transaction |
| 9 | Reference ranges | PASS | Copied onto the result, not linked — a revised range cannot reinterpret an old result |
| 9 | Payment gate on specimens | FIXED (contract) | The unpaid-override reason was not on the published interface |
| 8 | Prescription immutability | PASS | Issued prescriptions version rather than edit; print counted on dialog close |
| 10 | FEFO / oversell | PASS | `FefoAllocator` locks in expiry order and refuses to run outside a transaction |
| 10 | Dispensing reversal | PASS | Returns to the originating batches; double reversal blocked by unique key |
| 10 | Dispensing audit | FIXED | Reversal was audited, dispensing was not |
| 10 | Role defaults | FIXED | `pharmacist` was granted nothing by the Pharmacy module; fixed in code and applied to the live install 2026-09-08 |

## Money

| # | Area | Status | How |
|---|---|---|---|
| 11 | Invoice arithmetic | PASS | Read `InvoiceCalculator`; discount before tax, allocation sums back exactly. Also verified against live data: all 11 invoices foot to the cent |
| 11 | Invoice arithmetic tests | FIXED | `InvoiceCalculatorTest` imported a class that had moved, so five tests had been erroring rather than running |
| 11 | Overpayment | FIXED | Guard read a stale snapshot; two cashiers could both take the full balance |
| 11 | Over-refund | FIXED | Same; second refund committed then broke the invoice's totals |
| 11 | Double reversal | FIXED | Same shape; now under the invoice lock |
| 11 | Over-crediting | FIXED | Same; credit notes now guard under the lock |
| 11 | Cash sessions | PASS | One open till per cashier, locked; reversal lands in today's shift, not a closed one |
| 24 | Appointment double-booking | PASS | `lockDoctorDay()` serialises bookings for one clinician's day |
| 24 | Sequence gaps | PASS | `SequenceGenerator` locks; `ReconcileSequencesTest` |

## Platform

| # | Area | Status | How |
|---|---|---|---|
| 12 | Messaging consent | PASS | Fails closed — no consent row means no send |
| 13 | PDF generation | PARTIAL | Report and prescription verified by rendering and extracting text; invoice and credit-note PDFs not re-verified this pass |
| 14 | Document authenticity | PASS | Registered, hashed, signed, token-addressed; masked identity on the public page |
| 14 | Public verification page | FIXED | Every genuine QR redirected to the home page for a guest; eager loads re-applied the branch scope. Found 2026-09-08, after this row said PASS |
| 14 | Patient upload portal | FIXED | Same shape, same day: every genuine upload link redirected. 5 live links affected |
| 15 | Audit immutability | PASS | Hash-chained under a lock; model refuses update and delete |
| 15 | Audit coverage | FIXED | Dispensing was missing |
| 16 | Injection | PASS | Every raw SQL fragment read: all parameterised or constant |
| 16 | XSS | PASS | Three unescaped outputs, all read: escaped URL, Fortify SVG, hex-validated CSS |
| 16 | Debug / secrets | PASS | `APP_DEBUG=false`, `APP_ENV=production`, sessions encrypted and secure |
| 16 | Update supply chain | PASS | Signature bound to version + digest + size; fails closed with no key |
| 19 | N+1 | PASS | `preventLazyLoading` outside production, so the suite catches them |
| 21 | Localisation | PARTIAL | Duplicate `full_name` key fixed in both languages; RTL PDF output not re-verified this pass |
| 22 | Backups | PARTIAL | Daily local dumps verified previously; **no off-site copy, no alerting** |
| 28 | Quality gates | FIXED | Pint, PHPStan and deptrac were not all green; they are now |

## Not established

| Area | Why |
|---|---|
| Restore rehearsal | Requires a destructive operation on a live database. Not attempted; needs a scheduled window |
| SMTP delivery to a real server | Configuration path verified end to end (13 tests); no live mailbox to send to |
| Off-site backup | `backups-remote` has no credentials |
| Invoice / credit-note PDF rendering | Not re-rendered this pass |
| Arabic PDF layout | Not re-rendered this pass |
| Load and query profiling under real volume | Live data is small (8 patients, 13 orders); no meaningful profile available |

---

## Addendum — the state of the gates on arrival

Recorded because it explains several findings at once. When this audit started:

| Gate | State on arrival | Now |
|---|---|---|
| `composer lint` (Pint) | FAIL — 5 committed files | PASS |
| `composer analyse` (PHPStan 6) | FAIL — 9 errors | PASS |
| `composer deptrac` | PASS — 0 violations | PASS |
| `composer test` (Pest) | FAIL — 5 errors in `InvoiceCalculatorTest` | PASS |

Three of the four gates the project defines for itself were red, and had been for
long enough that the errors were load-bearing rather than new. Two of the nine
PHPStan errors were real defects reported here — the specimen override missing
from its published interface, and the duplicate translation key. The five test
errors were the invoice arithmetic suite failing to load at all.

A red gate stops being information. That is the mechanism by which the other
findings survived, and restoring all four to green is the part of this audit most
likely to keep paying.

---

## Addendum, 2026-09-08 — a PASS that was wrong

Row 14 above said **Document authenticity — PASS**. Two days later both public
pages turned out to be broken for every guest who reaches them, and had been.
The full account is the 2026-09-08 postscript in
[CMS-AUDIT-PROGRESS.md](CMS-AUDIT-PROGRESS.md).

The mechanism is worth stating here, next to the table it damages: the suites
that earned that PASS set a branch context in `beforeEach`, because the fixtures
need one, and never cleared it. A guest has no branch context. So the pages were
tested in a state they are never used in, and the tests were right about
everything except the one thing that mattered.

Nothing in this checklist distinguishes a row that was reasoned about from a row
that was tested in the wrong conditions. When reading any **PASS** here, the
question worth asking is not "was there a test?" but **"was it run in the state
production is in?"** — that is the question that would have caught this one.
