# Plan — make Client » Process » Facility » Provider a cross‑vertical foundation

**Status:** proposal for review (no code changed yet).
**Goal (from you):** the c»p»f»p mapping should serve **all verticals**, not just Coding — specifically:
1. **Shared/global facilities & providers** (rename the `coding_*` tables to global names).
2. The **Hierarchy** management page usable for **AR & Billing**, not just Coding.
3. **User alignment** (scope a person to a specific c»p»f»p) for **all verticals'** users.
**Hard guardrail that must survive:** a coder must still **never** see AR/Billing facilities (and vice‑versa).

---

## 1. What the codebase actually looks like today (facts from the audit)

- **Clients & processes are already global** (a client carries `clients.vertical_id`; a process belongs to a client). **Facilities, providers, their links, and user alignment are Coding‑owned.**
- **Tables involved:** `coding_facilities` (154 rows), `coding_providers` (197 rows, a **flat global list**, no vertical column), `coding_facility_providers` (643 rows, the M:N link), `coding_user_scope` (462 rows across 66 users — the **only** alignment table in the whole app).
- **A facility belongs to exactly one process** (`coding_facilities.process_id` FK, `ON DELETE CASCADE`) → one client → one vertical. So a facility's vertical is **derived by the JOIN chain**, not stored. Providers have **no** vertical link at all — a provider is "coding" only because it's *linked* to a coding facility.
- **The vertical guardrail is not centralized.** The literal `(v.code='CODING' OR v.name='Coding')` is **copy‑pasted into ~10 query sites** across `includes/coding.php`, `coding-hierarchy.php`, `coding-facilities.php`, `coding-access.php`, `coding-reports.php`. There is **no shared helper**. This is the single biggest hazard: relax it wrong in one place and a coder sees AR/Billing data, or an AR user is blocked.
- **Rename blast radius:** ~**154 literal occurrences across 22 files** (`includes/coding.php` + 21 `admin/*.php`). All **DDL and writes (16 statements) are in `coding.php`**; the other ~138 are **read‑only** JOIN/SELECT across the admin pages. Heaviest readers: `coding-validate.php` (14), `coding-audit.php` (9), `coding-access.php` (9), `coding-deliverables.php` (8).
- **Hidden references with NO foreign key** (a schema tool won't catch these — only the JOIN code does): `coding_billing_sheets.facility_id/provider_id`, `coding_user_scope.facility_id/provider_id`, and `coding_inventory.{coded,pcp,rendering,billing}_provider_id`.
- **Real foreign keys into the 3 tables (6 total):** `coding_inventory.facility_id/provider_id` (**ON DELETE RESTRICT** — protects coded charts), `coding_cpt_validation.facility_id/provider_id` (CASCADE), and the junction's two self‑links (CASCADE). `RENAME TABLE` preserves these automatically.
- **AR has no facility/provider/client model at all.** `ar_allocation` is keyed by **`process_id` + `chart_number`** only — no `client_id`, `facility_id`, or `provider_id`. AR work is assigned by **allocation** (a supervisor pushes patients), not a standing alignment.
- **Billing already reads `coding_facilities`/`coding_providers`** (via released `coding_inventory` charts, which carry client/process/facility/provider). It has no facility/provider *management* page and no biller standing alignment.
- **No AR vertical row exists** in the DB (only `Billing` id1 and `Coding` id3). AR rides on whatever vertical its clients belong to.
- **No `ar.reference`/`billing.reference` and no `ar.access.manage`/`billing.access.manage` permissions.** And two **cleanup migrations actively DELETE cross‑department permission grants** on every deploy (`ar_allocate_revoke_billing_v1`, `ar_perms_ar_roles_only_v1`) — any new shared perm must be added in lockstep or it gets stripped.
- **Prod runs the old discarded build**; a deploy triggers `coding_legacy_reset_if_needed()` which **DROPs and rebuilds** the `coding_*` tables from a hardcoded drop‑list. Renaming tables means updating that drop‑list and the reset trigger too, or a deploy could fail to drop the old tables / wipe the new ones.

### The model you confirmed (2026‑06‑21) — fully shared master data, separation by user
Your clarification changes the model (and simplifies it):
- **One global set of master data.** A `client / process / facility / provider` created in **any** vertical is **shared across all verticals** — there is ONE "ABC Hospital", used by Coding *and* AR *and* Billing *and* any future vertical. So `clients.vertical_id` stops being an *owner/fence*; master data is vertical‑agnostic.
- **Verticals are data‑driven and open‑ended.** More verticals beyond AR/Coding/Billing will be added, so **nothing may hardcode the vertical list** (no `CODING`/`AR`/`BILLING` constants in logic) — everything reads the `verticals` table.
- **Separation moves from the DATA to the USER + WORK.** A user belongs to a vertical **via their role**. A user is *aligned* to specific c»p»f»p (from the shared tree). What they see = **their vertical's work** (coding charts vs AR claims vs billing) **for their aligned c»p»f»p**. So a coder and an AR worker can both be aligned to "ABC Hospital", but the coder only sees coding work there and the AR worker only AR work — the old "coder can't see AR *clients*" rule becomes "a user only sees **their vertical's work**, scoped to **their alignment**." This is cleaner than per‑vertical client ownership and removes the hardcoded `CODING` fence entirely.

**Assignment flow (you asked which of two):** recommended = **pick the user first → the system derives their vertical(s) from their role → then pick c»p»f»p from the shared tree.** If the user holds roles in **more than one** vertical, it asks **which vertical** first (your option b), then c»p»f»p. Single‑vertical users skip that step (your option a). The alignment row is therefore **(user, vertical, client, process, facility, provider)** so one person can have different c»p»f»p per vertical, and it's future‑proof for N verticals.

> Note: this supersedes §2 Fork "shared vs per‑vertical facilities" below (resolved = fully shared) and the old "coder sees only Coding clients" guardrail — see the revised guardrail in §3/§4.

---

## 2. What's decided vs still open (after the 2026‑06‑21 clarification)

**Decided** (the model box above is now the source of truth):
- **Master data is fully shared.** A `client/process/facility/provider` created in **any** vertical is usable by **all** verticals — one global tree. `clients.vertical_id` stops being an owner/fence (it becomes, at most, "where it was created").
- **Verticals are data‑driven and open‑ended.** Nothing may hardcode `CODING/AR/BILLING`; logic reads the `verticals` table so new verticals slot in with no code change.
- **Separation is by USER (role→vertical) + WORK (which work table), not by master data.** A coder and an AR worker can both be aligned to "ABC Hospital"; the coder sees only coding charts there, the AR worker only AR claims. The old "coder can't see AR *clients*" rule is gone; the rule is now "a user sees only **their vertical's work**, scoped to **their alignment**."
- **Assignment flow:** pick the **user** first → system derives their vertical(s) from their role → if they hold roles in **more than one** vertical, choose the vertical → then pick c»p»f»p from the shared tree. Alignment row becomes **(user, vertical, client, process, facility, provider)**.

**Still open (I have a safe default for each):**
- **A. Table names** — rename `coding_*`→global (`facilities`/`providers`/`facility_providers`/`user_scope`) or keep the `coding_*` names. Now that the data is truly shared the prefix is misleading, *but* the rename is the single riskiest step (154 refs, live system). **Default: keep the names for now, rename in a later dedicated pass.**
- **B. AR work depth** — shared master data lets AR *have* facilities/providers, but AR's *work* row (`ar_allocation`) is **process‑only**, so AR work/reports can't be scoped below process until columns are added. **Default: align AR at process level now; add facility/provider to AR work later.**
- **C. Missing vertical rows** — deriving "user's vertical from role" needs every vertical to exist as a `verticals` row; today only Billing & Coding do. **Default: add the missing rows (AR, and any future ones) as part of Phase 0.**

---

## Progress (2026‑06‑21)
- **Phase 0 — DONE & verified.** `includes/verticals.php` added (data‑driven: `app_verticals`, `app_user_vertical_ids`, `can_manage_master_data`); AR vertical seeded (migration `verticals_seed_operational_v1`). Role→vertical derivation works (admin→all, coder→CODING).
- **Phase 1 — DONE & verified.** Vertical fence removed from the master‑data reads + both tree builders (`coding_accessible_tree`, `coding_facility_tree_build`) + `coding-facilities.php` + `coding-hierarchy.php`. Restricted coder (uid 2) still sees only their 2 aligned clients (alignment still fences); full tree now unfenced (7 clients).
- **Phase 2 — DONE & verified.** Master‑data pages gated by a data‑driven `__masterdata__` marker → `can_manage_master_data()`; MASTER DATA nav shows for any vertical's supervisor. Billing & AR supervisors now reach Hierarchy/Facilities/Providers (HTTP 200); a plain coder is still denied (302).
- **Phase 3 — DONE & verified.** `coding_user_scope.vertical_id` added + backfilled to Coding (462 rows). Scope checks (`coding_user_can_tuple`, both tree builders, the billing-sheet scope, the report scope, coding-access dedup/tier/replace/display) now fence to the CODING vertical, so a user's non-coding alignment can't grant coding access (verified: a billing-vertical row for coder uid 2 → coding can_tuple = NO, coding picker unchanged). New `admin/user-alignment.php` (SysAdmin) implements the flow: pick user → derive vertical(s) from role → if >1 choose → pick c»p»f»p from the shared tree → row = (user, vertical, c»p»f»p). Verified: per-user vertical derivation (coder→Coding only, billing→Billing only), a user can only be aligned to verticals their role grants.
- **Phase 4 — DONE.** Consumers (Reconciliation report, inventory billing-sheet strip, coding-access) read only their vertical's alignment rows; `coding-access.php` writes/dedupes/replaces vertical-aware (Coding).
- **ALL PHASES COMPLETE & verified non-breaking.** Coding behaves identically for existing coders; cross-vertical alignment + shared master data + cross-vertical hierarchy all in place.
- **Post-build adversarial review hardening (2026-06-21).** A review caught that resolving the Coding vertical id at runtime (`app_vertical_id_by_code('CODING') ?? 0`) would make the scope fence `vertical_id=0` and silently strip ALL coder access if the Coding vertical row were ever soft-deleted/re-id'd (reachable via the vertical-dedupe migration). FIX: all coding scope READS now fence via `coding_vertical_scope_sql($col)` = `($col IS NULL OR $col IN (SELECT id FROM verticals WHERE code='CODING' OR name='Coding'))` — matched by code, immune to id changes/soft-delete. INSERTS stamp the active Coding id or NULL (never 0). Verified: with the Coding vertical soft-deleted, a coder still reaches their aligned client; a Billing alignment still grants no coding access. Also tightened `user-alignment.php` to SysAdmin-only and fixed stale "coding shows only Coding clients" comments (the shared-tree behavior is intended).

## 3. Revised phased plan (shared data · data‑driven verticals · separation by user+work)

Each phase is independently shippable and verified by the cross‑vertical leak test (a user must never reach another vertical's *work*).

### Phase 0 — Make the vertical model data‑driven *(no hardcoded constants)*
- Replace the `CODING_VERTICAL_CODE/NAME` constants in **logic** with helpers backed by the `verticals` table: `all_verticals()`, `vertical_of_user($uid)` (derives the vertical(s) a user works from their roles), and a data‑driven **role→vertical** map so new verticals' roles register without code edits. Seed the missing vertical rows (decision C).
- **Verify:** every coding query still resolves to Coding for coders (behavior unchanged); nothing references a hardcoded vertical string in logic anymore.

### Phase 1 — Unfence master data (c»p»f»p becomes global)
- Remove the `(v.code='CODING'…)` fence from the **master‑data** reads (clients/processes/facilities/providers lists, the Hierarchy/Facilities/Providers pages, and the picker trees) so the one shared tree is visible system‑wide.
- **Keep each user's data scoped by their alignment** (`coding_user_scope` already does this) — only the *vertical fence* is removed, never the *alignment*. A restricted coder still sees only their aligned c»p»f»p; an unrestricted admin now sees the full shared tree.
- **Verify:** a coder's pickers show only their aligned c»p»f»p; an admin sees the shared tree; **a coder's WORK views still show only coding charts** (separation now = work table + alignment).

### Phase 2 — Hierarchy / Facilities / Providers usable by every vertical
- Open the three management pages to each vertical's reference‑admins via a **data‑driven `<vertical>.reference` capability** (widen route gates + nav; update the AR/Billing separation cleanup migrations so the new perms survive a deploy). Master data created anywhere is immediately shared.

### Phase 3 — Cross‑vertical user alignment + the assignment UI
- Extend the alignment to **(user, vertical, client, process, facility, provider)** — add the `vertical` column (rename `coding_user_scope`→`user_scope` only if fork A1; else keep). Move it out of the coding legacy‑reset's ownership so non‑coding alignments aren't wiped on a coding redeploy.
- Build the assignment flow exactly as you described: **pick user → derive vertical(s) from role → (if >1) choose vertical → pick c»p»f»p from the shared tree.** Add a data‑driven `<vertical>.access.manage` capability; the "unrestricted" bypass becomes per‑vertical (`<vertical>.supervise`), all derived from data — no hardcoded vertical.
- **Verify:** a two‑vertical user gets the vertical step and can hold different c»p»f»p per vertical; a single‑vertical user skips it; each user sees only their vertical's work for their alignment.

### Phase 4 — Consumers (work + reports) read the generalized alignment
- Point each vertical's work pages + role‑scoped reports (the Reconciliation‑report pattern) at the generalized alignment. AR facility/provider **work** columns land here if you pick fork B1.

---

## 4. Risks & mitigations (live medical system)

| Risk | Mitigation |
|---|---|
| **Wrong‑vertical work leak** (a coder reaches AR/Billing work) | Work lives in separate tables (`coding_inventory`/`ar_allocation`/billing) gated by per‑vertical perms — separation is structural. Leak test after Phases 1–3: a coder's nav/queries never touch other verticals' work tables, and alignment is applied per vertical. |
| Hardcoded vertical creeping back in | Phase 0 makes verticals data‑driven first; grep‑gate that no `'CODING'/'AR'/'BILLING'` literal remains in logic. |
| Removing the master‑data fence over‑exposes data | Only the *vertical* fence is removed; each user's *alignment* still scopes their tree, so restricted users are unaffected — verified before/after snapshot per page. |
| Hidden no‑FK columns (billing‑sheets, user‑scope, the 4 inventory provider cols) | Enumerated in §1; each JOIN audited by code, not FK tooling. |
| Prod old‑build legacy‑reset drops/rebuilds these tables | Update the drop‑list + reset trigger in lockstep; move `user_scope` out of coding's reset so other verticals' alignment survives a coding redeploy. |
| Separation cleanup migrations strip new shared perms | Update `ar_perms_ar_roles_only_v1` / `ar_allocate_revoke_billing_v1` in the same change (or make them vertical‑aware). |
| (only fork A1) one missed table‑name string → "table doesn't exist" in prod | Atomic `RENAME TABLE` + grep‑gate (zero old names remain) + full page‑load smoke. Default A2 avoids this entirely. |
| Prod schema ≠ local | Re‑verify columns/FKs against **prod** before any migration ships. |

## 5. Verification & rollback
- Per‑phase: snapshot each affected page's data **before**, diff **after** (restricted users unchanged); `php -l` all touched files; page‑load smoke as coder + supervisor + sysadmin + AR + billing users; the cross‑vertical work‑leak test.
- Rollback: each phase is a revertible commit; a `RENAME TABLE` (if A1) reverses by renaming back.

## 6. The three calls I still need (defaults in brackets)
1. **Table names** — rename to global, or keep `coding_*`? **[keep for now]**
2. **AR work depth** — facility/provider on AR work now, or process‑level alignment first? **[process‑level first]**
3. **Missing vertical rows / sequence** — OK to seed the missing vertical rows and start **Phase 0 → 1 → 2 → 3**, doing Billing end‑to‑end first (it already carries c»p»f»p) with AR work‑columns as a follow‑up? **[yes]**

If you're happy with the three defaults, just say "go with the defaults" and I'll start at Phase 0 (the safe, data‑driven groundwork) and check in after each phase.
