# MedPortal — Patient Portal (staff-side management + data layer)

SIFA plan **Phase 8 — `patient_portal`** (achievable core). A Laravel nwidart
module that adds the patient-facing STAFF-SIDE management surfaces plus the
record-release / messaging / health-tools **data layer**, and a thin
patient-facing web view.

> **Reuse-first / module-only.** MedPortal **binds** to existing engines — it
> never rebuilds them:
> - **Authentication & booking:** `Modules/PortalApi` already owns portal
>   accounts (`portal_owners`), OTP challenges, tokens, brands and the
>   `BookingDriver` registry (human clinic = `ClcBookingDriver`). MedPortal does
>   **not** re-implement any of that.
> - **Clinical data:** `Modules/ClinicCore` owns patients, appointments,
>   prescriptions, medical reports, vitals, conditions, allergies and imaging
>   reports. MedPortal reads them **fail-soft** (read-only, no FK constraints).
> - The ERP **core stays byte-identical** — zero changes outside
>   `Modules/MedPortal`.

Dependency: `requires: ["ClinicCore"]`. Install gate: the lowercase
`medportal_version` System property (superadmin Modules screen).

---

## What's REAL vs STUB

| Area | Status | Notes |
|---|---|---|
| Dependents (family) | **REAL** | Guardian contact ↔ dependent patients, with can_book / can_view flags. |
| Record release control | **REAL** | Provider-controlled visibility per record type; see gating below. |
| Secure messaging | **REAL** | Patient↔provider threads, reply, read tracking; MedNotify ping fail-soft. |
| Health self-tracking | **REAL** | weight / BP / glucose / temperature / mood / steps logs + trend chart. |
| Questionnaires (PHQ-9 / GAD-7) | **REAL** | Standard items + scoring bands; severity + PHQ-9 ≥ 15 dashboard flag. |
| Temporary record share | **REAL (data layer)** | Tokenised, expiring share grants — issue / revoke lifecycle. The external-provider *viewing endpoint* is thin/backlog. |
| **Telemedicine** | **STUB — BACKLOG** | "Generate link" stores a **placeholder** `join_url` only. No real video (Zoom/Meet/Teams/WebRTC), no waiting room, no in-call chat. |
| **Online payment** | **STUB — BACKLOG** | Records a payment **intent** and lets staff **mark paid manually**. No real gateway; the ERP invoice (`transactions`) stays the source of truth. |
| Symptom checker / med reminders / educational content | **BACKLOG** | Out of achievable-core scope; not built. |

### Backlog (documented, intentionally not built)
- Real telemedicine video integration + virtual waiting room + in-call chat.
- Real payment-gateway integration (Stripe / PayTabs / Fawry …) with callbacks.
- Symptom-checker triage and medication-reminder scheduling (cron).
- The external-provider record-viewing endpoint behind a `share_grants` token.
- Wiring the patient-facing web routes to PortalApi token auth (see below).

---

## How RecordAccessService gates visibility

`Services/RecordAccessService` is the single source of truth for **what a portal
patient may see**. Per the spec: *clinical notes only after provider release;
labs / imaging / prescriptions / vitals visible by default.*

1. **Config defaults** (`Config/config.php → release_defaults`):
   `clinical_note = false`, everything else `= true`, `full = false`.
2. **Per-patient overrides** (`mportal_record_releases`): a row overrides the
   default for one `(patient, record_type)` — `released=true` reveals early,
   `released=false` hides a normally-visible type.
3. **`full` master toggle**: a `full` row set `released=true` reveals all types
   (still overridable per-type).

`visibleRecords($businessId, $patientId)` returns a `type => bool` map; the
staff **Record Release** screen toggles it, and the patient-facing controller
queries a ClinicCore block **only when its type is visible**. Nothing is read
from ClinicCore until the gate says yes.

---

## Patient-side wiring status

Full portal login is **PortalApi's** job (portal_owners + OTP + tokens),
consumed by the Vue PWAs over REST. On the ERP **web** stack that token auth may
not be wired, so `Http/Controllers/MeController` (routes under `med-portal/me`)
is deliberately **thin and defensive**:

- It authenticates via the **`customer` session guard** (`config/auth.php` →
  provider `contacts`) **when available**.
- If the guard is absent or no contact is authenticated, it renders a
  **"portal login required"** page instead of erroring (wrapped in try/catch).
- Everything it shows is **release-gated** by `RecordAccessService`.

This keeps the **staff side fully functional today** and the **patient side
API-ready**: the same release gate + repositories back a future PortalApi
endpoint. To go live for patients, wire the `customer` guard login (or expose
these reads through a PortalApi controller) — no schema change required.

---

## Tables (all prefix `mportal_`, business_id-scoped)

| Table | Purpose |
|---|---|
| `mportal_dependents` | guardian contact ↔ dependent patient links |
| `mportal_record_releases` | per-patient per-type release overrides |
| `mportal_messages` | secure patient↔provider messages (thread_id UUID) |
| `mportal_health_logs` | wellness self-tracking readings |
| `mportal_questionnaires` | questionnaire defs (PHQ-9 / GAD-7 system rows + custom) |
| `mportal_questionnaire_responses` | patient answers + score + severity |
| `mportal_share_grants` | tokenised temporary record shares |
| `mportal_telemedicine` | **STUB** appointment join-link |
| `mportal_payment_intents` | **STUB** online-payment intents |

Migrations are dated `2026_07_08_00xx`. References into ERP / ClinicCore tables
(`contacts`, `clc_patients`, `clc_appointments`, `transactions`, `users`) are
plain indexed columns — **no cross-table FK constraints** — matching the module
convention.

## Permissions

`mportal.manage`, `mportal.records.release`, `mportal.messages`,
`mportal.dependents.manage`, `mportal.health.view`,
`mportal.questionnaires.manage`. Registered by migration
`2026_07_08_0010` + `MedportalPermissionSeeder`; surfaced in the role editor via
`DataController::user_permissions()`. Admins bypass via the core Gate::before.

## Menu

`DataController::modifyAdminMenu()` injects a **Patient Portal** dropdown
(Dashboard · Record Release · Messages · Dependents · Health Tracking ·
Questionnaires) at order ~48, gated by the SaaS entitlement helper (guarded with
`function_exists`), the `medportal_module` subscription flag and the `mportal.*`
permissions — exactly like MedBilling.

## Seeders

- `MedportalPermissionSeeder` — registers the `mportal.*` permissions.
- `MedportalQuestionnaireSeeder` — seeds the **PHQ-9** and **GAD-7** system
  questionnaires (items + Likert options + standard scoring bands).
- `MedportalNotifyEventSeeder` — registers `portal.message_received` with
  MedNotify **fail-soft** (no-op without MedNotify).
- `MedportalDemoSeeder` — for the **`Shifa One`** business only (**warns**, not
  silent, elsewhere): 1–2 dependents, a few record releases, 2 message threads,
  a **moderate** PHQ-9 response, weight + BP health series, 1 telemedicine link
  (stub), 1 payment intent (stub), 1 share grant. Idempotent.

## Install

Superadmin → Modules → MedPortal → Install. Runs
`module:migrate/publish/seed MedPortal`, writes `medportal_version`, and grants
`mportal.*` to the Admin role. Repair re-runs migrations idempotently.
