# Clinic CMS — system inventory

What this system is, as built, established by reading the code, the schema and
the running installation rather than the documentation. Where the two disagree
the code wins and the disagreement is noted.

Compiled 2026-09-03/04 against `main` at `213f7ab`, with the working tree
carrying uncommitted document-verification and branding work.

Findings and fixes live in [CMS-AUDIT-PROGRESS.md](CMS-AUDIT-PROGRESS.md) and
[CMS-FINAL-AUDIT.md](CMS-FINAL-AUDIT.md). This file is the map.

---

## Shape

Laravel 12 modular monolith. 1,100 PHP files under `app/`, 138 Blade templates,
124 test files, 83 migrations, 79 tables.

```
app/
  Foundation/    shared kernel — value objects, repository base, billing ports
  Support/       audit engine, Money, sequences, branch context, documents
  ModuleKernel/  module discovery, route loading, navigation, licence gates
  Models/        core models: User, Branch, AuditLog, IssuedDocument
  Modules/       fourteen modules, each a slice of the product
```

### Modules

| Module | Files | What it owns |
|---|---|---|
| Laboratory | 173 | Catalogue, panels, ranges, specimens, results, verification, reports |
| Billing | 130 | Services, tax, invoices, payments, credit notes, cash sessions |
| Patients | 96 | Registration, MRN, search, documents, problem list, allergies |
| Auth | 92 | Users, roles, permissions, sessions, API tokens, passkeys, 2FA |
| Consultations | 82 | Encounters, notes, vitals, diagnoses, triage |
| Prescriptions | 82 | Formulary, prescriptions, protocols, printing |
| Appointments | 78 | Schedules, exceptions, walk-ins, queue, public booking |
| Messaging | 43 | Templates, consent, outbox, dispatch |
| Licensing | 41 | Activation against this installation |
| Pharmacy | 39 | Stock, batches, expiry, FEFO dispensing |
| Updates | 37 | Signed release check, download, install, rollback |
| Cdss | 35 | Decision-support rules and suggestion events |
| Settings | 31 | Clinic configuration, branding, backups |
| Portal | 21 | Patient upload links and review |

### Layering, and what enforces it

`deptrac.yaml` declares Controller → Service → Repository/Query → Model, with
`Foundation` beneath and `Kernel` beside. Controllers may not touch Eloquent or
open transactions; services own the transaction boundary; models may not reach
services.

Held by `composer deptrac` (0 violations) and by `tests/Architecture/`:

- `ModuleBoundaryTest` — core may never import `App\Modules\*`; modules reach
  each other only through `Contracts` and `Events`.
- `ApiPlatformTest` — API controllers return `JsonResponse`, extend the platform
  base, no route closures, resources on allow-lists.
- `ContentSecurityTest` — no inline scripts or handlers, because the CSP refuses
  them.
- `BladeCompilesTest` — every template compiles.
- `AuthorizationCoverageTest` — **added by this audit**: every state-changing
  route decides who may.

---

## Tenancy — physical, not logical

One clinic, one installation, one database, one domain. There is no `clinic_id`
anywhere in the schema and nothing is written as though there could be.

`branches` is *inside* one clinic — a practice with satellite locations — and
`BelongsToBranch` scopes records between them with a global scope plus automatic
`branch_id` on create. Reading across branches is the explicit, greppable
`withoutBranchScope()`.

**This changes what "clinic isolation" means here.** Two clinics cannot leak into
each other because they are two databases on two servers; there is no query that
could get it wrong. The isolation that *is* enforced in code is between branches,
and that is what was audited.

Live install: one branch (`MAIN`, "My Clinic"), 8 patients, 13 lab orders, 11
invoices, 5 users.

---

## Data

79 tables. The conventions are consistent enough to state as rules:

- **Money** is `BIGINT` minor units plus a `char(3)` currency column, cast
  through `MoneyCast` into a `Money` value object. No float reaches a financial
  column. Rates are integers too — tax in basis points, discounts in hundredths
  of a percent, quantities in thousandths.
- **Public identifiers** are ULIDs. Sequential integer ids are never in a URL.
- **Document numbers** come from `SequenceGenerator` — locked, gapless, per
  branch and year, e.g. `LAB-MAIN-2026-00001`.
- **Foreign keys** are `RESTRICT` on anything a record's history depends on.
- **Corrections are versions.** `superseded_by_id` / `supersedes_id` / `version`
  on clinical notes, lab results and issued documents. Nothing clinical is
  overwritten.
- **Idempotency** is a unique `(branch_id, idempotency_key)` on `payments` and
  `dispensings`.

---

## Authentication and authorization

- Fortify for sign-in, with a single `CredentialVerifier` behind both the web
  form and API token issuance, so lockout and account-state rules cannot diverge.
- Sanctum for API tokens. 2FA and passkeys available.
- `spatie/laravel-permission` for roles and permissions: 80 permissions, 8 roles.
  An individual account may be given its own permission list that overrides its
  roles (`permissions_overridden`).
- 18 policies, registered per module.
- **Super Admin** bypasses every check via `Gate::before` by role name.
- **Account status** is the outer gate — see the finding about where it used to
  live.

Every module's `Routes/web.php` is loaded behind `['web', 'auth']` by the module
kernel; anything public must live in a separate, conspicuous `Routes/public.php`
or `Routes/api_public.php`. Only Appointments (public booking) and Portal
(patient uploads) have one.

---

## Core workflows

```
register → appointment/walk-in → triage → encounter → notes/diagnoses
                                              ├→ prescription → pharmacy dispense
                                              └→ lab order → specimen → result
                                                              → verify → report
                                       all of which raise charges →
                                       invoice → payment → receipt
                                       every step → audit_logs
```

Charges cross module boundaries through `ChargeCollector`, a core port, so the
laboratory and the pharmacy can bill without knowing invoices exist — and a
clinic without the billing module simply hands the medicine over.

Each counter keeps its own bill (`service_line`): the doctor's fee, the
laboratory's tests and the pharmacy's medicines are collected and receipted
separately.

---

## Documents, PDFs and verification

DomPDF for rendering; `endroid/qr-code` for the QR. Every official document is
registered in `issued_documents` with a number, version, content hash, stored
PDF and SHA-256, an ECDSA P-256 signature, and a random 32-byte verification
token.

Public verification at `/v/{token}` and `/verify`, throttled, showing masked
patient identity only. Uploading the PDF back re-checks its digest, which is what
detects a genuine QR pasted onto a forged report.

Covered by `docs/DOCUMENT-VERIFICATION.md` and by `ReportForgeryTest`,
`DocumentAuthenticityTest`, `DocumentVerificationPageTest`.

**Known limitation, documented and unresolved:** the signing key lives on disk
under `storage/app/private/keys` with no KMS.

---

## Audit trail

`audit_logs` is append-only and hash-chained: each row carries its predecessor's
hash, the chain read and the insert are locked together in one transaction, and
`AuditLog::update()` / `::delete()` throw. Secrets are redacted by
configuration before the row is written.

Audit writes never break the clinical action that triggered them — a failure is
logged loudly instead. That is a deliberate trade and it means a silent gap is
possible if logging itself fails.

`record_access_logs` separately records who read which patient record.

Live: 2,618 audit rows. Twelve known-broken chain entries from 2026-08-02 are
documented history and are never rewritten; nightly verification starts at id 147.

---

## Storage, jobs, notifications

- Disks: `private` (PHI, never web-served), `public` (branding), `backups`,
  `updates`, `backups-remote` (S3-compatible, **unconfigured**).
- Queue: database driver. `jobs` and `failed_jobs` both empty.
- Mail: `log` driver, which is the shipped default. Each clinic sets its own
  SMTP in Settings → Mail; `MailSettingsService::apply()` pushes the saved
  values onto the live config on every boot. Until an administrator does that,
  password resets go to the log — by design, so a fresh install cannot email
  strangers.
- Messaging (SMS/WhatsApp) is consent-gated and fails closed: no consent row
  means no send.

---

## Deployment

Single Windows/Laragon host serving `clinic.tmsoagency.com` from this very
directory. `APP_ENV=production`, `APP_DEBUG=false`, sessions encrypted and
secure-only, no pending migrations.

Two database accounts by design: `clinic_app` (no DDL) for the application,
`mysql_migrations` for schema changes. Config caching is deliberately not used
here; route caching hides module routes and is not used either.

Backups: verified daily dumps at 02:30 to `C:\laragon\backups\daily`.
**Local only — no off-site copy and no alerting.**

---

## Known gaps carried forward

These are stated as facts about the installation, not as fixes made:

- No off-site backup bucket and no backup alerting.
- Release signing key does not exist; the first was destroyed and blacklisted.
- Document signing key has no KMS.
