# MedBloodBank — Blood Bank (SIFA plan P6-T6)

Blood-bank management for the Shifa One platform: a donor registry, donation
collection with mandatory infectious-disease screening, crossmatch, issuance and
transfusion with a full adverse-reaction workflow, and a stock / expiry
dashboard.

**Ground rules honoured:** only `Modules/MedBloodBank` writes; the ERP core and
sibling modules stay byte-identical. `business_id` on every table. Migrations
dated `2026_07_05_80xx`. References into ERP / ClinicCore tables are plain indexed
columns (no cross-table FK constraints), matching the module convention.

Depends on **ClinicCore** (`module.json` → `requires: ["ClinicCore"]`).

---

## Reuse-first data model

| Concern | Where it lives | Reuse note |
|---|---|---|
| Donor person | ERP `contacts` (type customer/both) | A donor **is** a contact — never a parallel person table |
| Donor profile | `mbb_donors` (satellite, unique `contact_id`/business) | Mirrors ClinicCore's `clc_patient_contact_details` satellite |
| Recipient | ClinicCore `clc_patients` (`ClcPatient`) | Reused directly; picker searches patients |
| Blood bags | `mbb_donations` (spec-level inventory) | Tracked in-module; **never** auto-created as ERP products |
| Compatibility test | `mbb_crossmatches` | — |
| Issuance + transfusion + reaction | `mbb_issues` | — |

All tables are `business_id`-scoped and prefixed `mbb_`.

### Bags as inventory (ERP mirroring — optional, manual)

Bags are tracked as **spec-level inventory** inside `mbb_donations` (status,
group, product, expiry). The module deliberately **does not** auto-create ERP
products for bags. A business that wants bags to appear as ERP batch products can
mirror them manually: create a "Blood products" product per component with batch
tracking enabled and post opening stock keyed on `bag_no` as the batch/lot. This
module never writes to the ERP product / stock tables, so any such mirror is a
business-side, opt-in bookkeeping exercise and stays out of the clinical
lifecycle here.

---

## Lifecycle rules (all in `BloodBankService`)

### Donation lifecycle
```
collected → quarantined (auto on entry)
          → screening entry:
              all four markers negative → passed  → available
              any marker reactive       → failed  → discarded (+ donor permanently deferred)
          → crossmatch (compatible) → reserved
          → issue → issued → transfusing → transfused
          → expiry sweep → expired
```

### Screening (four mandatory markers: HIV, HBV, HCV, syphilis)
- **All negative** → `screening_status = passed`, bag → `available`.
- **Any reactive** → `screening_status = failed`, bag → `discarded`, and the donor
  is **permanently deferred** (`deferred_until` = today + 100y, reason + note).
- Any still `pending` → stays `quarantined` / `pending`.

### Donor eligibility (checked before a new donation is saved)
- Blocked if `deferred_until` is today or later (with reason).
- Blocked if `last_donation_at` is within `min_days_between_donations` (default
  **90** days) — the message quotes the next eligible date.

### ABO / Rh compatibility matrix (`compatibleDonorGroups`)
Standard RBC compatibility, most specific first:

| Recipient | Compatible donor groups |
|---|---|
| O− | O− |
| O+ | O+, O− |
| A− | A−, O− |
| A+ | A+, A−, O+, O− |
| B− | B−, O− |
| B+ | B+, B−, O+, O− |
| AB− | AB−, A−, B−, O− |
| AB+ | all (universal recipient) |

O− is the universal donor; AB+ the universal recipient.

### FIFO issuance suggestion (`suggestBags`)
Available, non-expired bags whose group is compatible with the recipient,
ordered **earliest expiry first**, then oldest collection. A `product_type`
filter is honoured when supplied.

### Crossmatch gate
Issuance requires a **compatible** crossmatch for that exact bag; an
`incompatible` (or missing) crossmatch blocks the issue. A compatible crossmatch
`reserves` the bag.

### Expiry
`expiryFor(product, collected_at)` derives shelf life (config-tunable):
whole_blood / prbc **+35d**, ffp / cryo **+365d**, platelets **+5d**. The daily
command `mbb:expire-bags` (scheduled 01:30) marks every holdable bag past its
`expiry_date` as `expired` (idempotent; also runnable from the dashboard button).

---

## Transfusion reaction → critical alert

When a transfusion is completed **with** a reaction, `completeTransfusion`:
1. flags the issue (`reaction = true` + type / severity / management), and
2. raises a **critical, patient-linked** alert via ClinicCore
   `MedicalAlertService::raise()` with `source_type = mbb_issues`,
   `source_id = <issue id>` — **idempotent per issue** (re-completing never
   duplicates the alert). Fail-soft if ClinicCore is absent.

Reaction types: febrile, allergic, hemolytic, **TACO**, TRALI, other.
Severities: mild, moderate, severe, life_threatening.

---

## Stock dashboard + low-stock notify

- `stockGrid()` — available-bag counts by **group × product_type**, each group's
  min threshold and a `low` flag (config `medbloodbank.min_stock`).
- `expiringSoon()` — available bags on the **7 / 15 / 30-day** expiry ladder.
- `notifyLowStock()` — fires a **fail-soft** MedNotify `bloodbank.stock_low`
  event per low group (no-op without MedNotify). Registered by
  `MbbNotifyEventSeeder`.

---

## UI map (routes under `med-blood-bank`, `mbb.*`)

| Screen | Route name | Permission |
|---|---|---|
| Stock dashboard (grid + expiring soon + sweep) | `mbb.dashboard` | any `mbb.*` |
| Donors CRUD (contact picker + satellite) | `mbb.donors.*` | `mbb.donors.manage` |
| Donations (entry + screening modal) | `mbb.donations.*` | `mbb.donations.manage` / `mbb.screening.result` |
| Crossmatch (patient picker + FIFO suggestion) | `mbb.crossmatch.*` | `mbb.crossmatch.perform` |
| Issues / transfusions (issue / start / complete + reaction modal) | `mbb.issues.*` | `mbb.issue.manage` |

Menu injected by `DataController::modifyAdminMenu()` under **"Blood Bank"**, gated
by the SaaS entitlement helper + the `medbloodbank_module` subscription flag + the
`mbb.*` permissions — exactly like MedBilling.

---

## Install / permissions

Standard superadmin install/update/repair endpoints
(`/med-blood-bank/install|update|repair`), gated by the lowercase
`medbloodbank_version` System property (`ModuleUtil::isModuleInstalled`).

Permissions: `mbb.donors.manage`, `mbb.donations.manage`, `mbb.screening.result`,
`mbb.crossmatch.perform`, `mbb.issue.manage` — registered by migration + seeder,
granted to `Admin` on install.

Seeders: `MbbPermissionSeeder`, `MbbNotifyEventSeeder` (registers
`bloodbank.stock_low` with MedNotify, fail-soft), `MbbDemoSeeder` (guarded on the
'Shifa One' business; reuses existing contacts + a patient, idempotent).

---

## Deviations flagged (Ground Rule 6)

1. **Bags are not auto-mirrored as ERP products** — bags are spec-level inventory
   in `mbb_donations`. ERP product mirroring is documented above as an opt-in,
   business-side exercise; this module never writes ERP product / stock tables.
2. **Reaction type `tacO` normalised to `taco`** — the spec wrote `tacO`; the
   enum / lang / views use lowercase `taco` (TACO = transfusion-associated
   circulatory overload) for consistency.
3. **Permanent deferral has no dedicated "forever" column** — it is modelled as
   `deferred_until = today + 100y` plus a note, since the spec defines only
   `deferred_until` / `deferral_reason`.
4. **`modules_statuses.json`** gains a `"MedBloodBank": true` entry — the standard
   nwidart module-registration requirement (peer Med* modules are registered the
   same way). No other file outside `Modules/MedBloodBank` is touched.
