# MedDocuments — Documents shared service (Shifa P2-T2)

A tenant-scoped document store for the medical platform: a system + custom
**doc-type taxonomy**, **versioning**, secure **expiring share links**, dynamic
**watermarking** (viewer name + datetime) on every view/print/download, and a
full **view / download / print / share / upload / delete audit trail**. It
exposes an attach/retrieve **service API** every other module calls.

Implements plan task **P2-T2** and REUSE-MAP §8 (documents): storage
backends/encryption are **reused from SmartStorage** (never reimplemented); the
doc-type taxonomy, version chain, watermarking and the document-layer audit are
the genuinely-new parts.

> Module-only: nothing outside `Modules/MedDocuments` is changed. No NeoSkin /
> TurboBoost coupling. Migrations are dated `2026_07_02_2000xx`. `module.json`
> `requires` is `[]`.

---

## Install

Install/update/repair endpoints mirror MedRegistry / ClinicCore. The install
gate is the **lowercase** `meddocuments_version` System property (the exact key
`ModuleUtil::isModuleInstalled()` reads). Install from the superadmin Modules
screen, or:

```
GET  /med-docs/install     # first install (idempotent)
GET  /med-docs/update      # semver-gated
GET  /med-docs/repair      # re-run migrations + seed
```

Install runs the migrations, publishes, seeds (permissions + 15 system types)
and assigns all `meddoc.*` permissions to the `Admin` role.

---

## Attach / retrieve API (for all modules)

Resolve the service from the container (`Modules\MedDocuments\Services\MedDocuments`,
also aliased `meddocuments`) and call it. This is the ONLY supported way to put
files into / get files out of the store — never touch the disk directly, so
audit + watermark always apply.

```php
use Modules\MedDocuments\Services\MedDocuments;

$svc = app(MedDocuments::class);

// 1) Attach a new document (creates the header + version 1). Returns the document.
$doc = $svc->attach($businessId, 'consent_form', $request->file('doc'), [
    'title'       => 'Surgery consent',
    'entity_type' => \Modules\ClinicCore\Entities\ClcAppointment::class, // generic polymorphic attach
    'entity_id'   => $appointment->id,
    'contact_id'  => $patientContactId,   // optional patient link
    'tags'        => ['surgery', 'signed'],
    'notes'       => 'Signed in clinic',
    'user_id'     => auth()->id(),
]);
// If the doc-type key is unknown for the business it is auto-created as a custom type.

// 2) Add a new version to an existing document.
$version = $svc->addVersion($doc, $request->file('doc'), auth()->user(), 'Re-scanned page 2');

// 3) Query documents attached to any entity (any module).
$docs = $svc->for(\Modules\ClinicCore\Entities\ClcAppointment::class, $appointment->id, $businessId)->get();

// 4) Stream the current (or a specific) version — logs the given action.
return $svc->stream($doc, $versionId = null, 'view', auth()->id());

// 5) Create a secure expiring share link. Returns a MeddocShare.
$share = $svc->share($doc, $days = 7, auth()->user());
$url   = $svc->shareUrl($share);   // https://.../med-docs/shared/{token}
```

Every call above writes a row to `meddoc_access_logs` (upload/view/download/
print/share/delete). File paths on disk follow
`meddocs/{business_id}/{document_id}/v{n}_{sanitized_filename}`.

### End-user routes (Blade, inside the ERP)

All under `med-docs/*`, name `meddoc.*`, guarded by `meddoc.*` permissions and
the same middleware stack as Vrc:

- `meddoc.documents.index` — browser (DataTable + upload/version/share modals)
- `meddoc.documents.view` — **watermarked** inline view (image = GD overlay,
  pdf = mPDF stamp, else HTML viewer w/ CSS overlay)
- `meddoc.documents.print` — watermarked print page
- `meddoc.documents.download` — watermarked download
- `meddoc.documents.download-original` — **raw** download, requires
  `meddoc.download_original`
- `meddoc.types.*` — doc-type CRUD (system rows are read-only)
- `meddoc.audit.index` — access-audit trail
- `meddoc.shared.show` / `.view` — **public** tokenized share (no login;
  expiry + revocation checked; logged; watermarked view only)

---

## Storage resolution (reuse-first)

`MedDocuments::diskName()` resolves the target disk:

1. **Explicit override** — `config('meddocuments.storage_disk')` always wins.
2. **SmartStorage installed** (`system.smartstorage_version` set) →
   `config('smartstorage.target_disk')`, **but only when that disk's root is
   NOT under the public web root**. On this core the plain `local` disk is
   rooted at `public_path('uploads')`, and an installed-but-inactive
   SmartStorage does not intercept writes there — medical documents must never
   land under a guessable `/uploads/...` URL, so a publicly-rooted target is
   refused. When SmartStorage's disk qualifies, its **backends
   (local/S3/Dropbox/GDrive), at-rest AES-GCM encryption, and file-level
   `ss_access_logs` all apply automatically** — this module does not
   re-implement any of it.
3. **Otherwise** → a private `meddoc` disk registered at runtime by the service
   provider (additively; `config/filesystems.php` is never edited), rooted at
   `storage_path('app')` (stored paths carry the `meddocs/{business}/{doc}`
   prefix) — **off the public web root**.

The disk a version was written on is recorded in
`meddoc_versions.storage_disk`, so retrieval resolves the right adapter even if
the tenant later switches SmartStorage backends.

Either way, **no file is ever served from a public path** — all access goes
through the controller routes, which watermark and audit.

The `meddoc_access_logs` table is the **document-layer** audit (every access
path, regardless of backend). When SmartStorage is active it complements the
**file-layer** `ss_access_logs`.

---

## Watermarking — chosen approach & limitation

Pragmatic per REUSE-MAP §8. A PDF lib check found **mPDF** (`mpdf/mpdf ^8.1`)
and **dompdf** (`barryvdh/laravel-dompdf ^2.0`) installed; **TCPDF is not**
present (only the FPDI→TCPDF bridge). mPDF is used because it is already the
ERP's PDF engine (`Controller::getMpdf()`) and its bundled `FpdiTrait`
(`SetSourceFile` / `ImportPage` / `UseTemplate`) can import an existing PDF and
overlay a watermark on every page — no extra dependency.

- **Images** → **GD overlay** (`Services\Watermarker::image`): the viewer name +
  datetime is drawn diagonally, tiled, semi-transparent, **onto the pixels** and
  re-encoded to PNG. The served bytes are watermarked, not just CSS.
- **PDFs** → **mPDF stamp** (`Services\Watermarker::pdf`): every source page is
  imported and a rotated, faint (`watermarkTextAlpha = 0.12`) caption is laid
  over it.
- **Fallback** → if GD/mPDF is unavailable, the PDF can't be re-rendered, or the
  file is neither image nor PDF, the **HTML viewer** page
  (`public/viewer.blade.php`) embeds the document under a **diagonal CSS
  watermark overlay** carrying the same caption.

**Raw, unwatermarked bytes** are reachable only via
`meddoc.documents.download-original`, gated by the `meddoc.download_original`
permission.

### Limitation (honest)

The watermark is a **deterrence / provenance** measure, not DRM:

- A user granted `meddoc.download_original` can always obtain the unmarked file.
- Screen capture defeats any on-screen (CSS) watermark; the pixel/vector
  watermark on images/PDFs is more durable but still not tamper-proof.
- The CSS-overlay fallback watermarks the *presentation*, not the file bytes.
- Very large files or unusual/encrypted PDFs may fail mPDF re-rendering and
  degrade to the HTML-overlay viewer.
- e-signature (spec §8) is out of scope for P2-T2 — `meddoc_types.requires_signature`
  is a metadata flag only; a signature workflow can reuse ClinicCore's
  `clc_consents` pattern in a later task.

---

## Tables (all `business_id`-scoped, session-scoped in controllers)

| Table | Purpose |
|---|---|
| `meddoc_types` | Doc-type taxonomy. `business_id` NULL = system type (15 seeded, read-only). |
| `meddoc_documents` | Document header + generic polymorphic attach (`entity_type`/`entity_id`) + `contact_id` patient link + `tags` + soft deletes. |
| `meddoc_versions` | Version chain (one physical file per row; `storage_disk` recorded). |
| `meddoc_access_logs` | Insert-only audit (view/download/print/upload/share/delete). |
| `meddoc_shares` | Secure share links (64-char token, expiry, revocation, open count). |

## Permissions

`meddoc.view`, `meddoc.upload`, `meddoc.manage_types`, `meddoc.share`,
`meddoc.download_original`, `meddoc.audit.view`.

## Deviations flagged (plan Ground Rule 6)

- **`entity_type` / `entity_id`** are a lightweight generic attach (as the task
  specifies), not a Laravel `morphMap` — kept intentionally simple so any module
  can attach without registering a morph alias.
- **Retention / e-signature** are stored as metadata only (`retention_days`,
  `requires_signature`); enforcement/workflow are later tasks, not P2-T2.
- **Private `meddoc` disk**: to keep files off the public web root without
  editing core `filesystems.php`, the disk config is registered additively at
  runtime in the service provider. This is config-in-memory only; no core file
  changes.
