---
title: elmadina Onboarding — live lab provisioned from moonui
slug: elmadina-onboarding
status: active
owner: hazem
updated: 2026-06-25
refs:
  - /home/moonui/*.php                       # the reusable cross-DB migration scripts (this session)
  - /home/moonui/db-backups/elmadina_db_*.sql.gz   # pre-op backups (one per step)
  - Modules/LIS/app/Services/LisCatalogService.php  # lossless catalog export/import
related:
  - middleware
  - lis-setup-screen
  - super-admin-owner
---

## Context
elmadina (`elmadina.elbaset.com`, cPanel user `elmadina`, BE at `/home/elmadina/public_html`, DB `elmadina_db`, **company_id=1**, admin `admin@gt4.com / 123456789`) is being stood up as a **live lab**, seeded FROM moonui (co4, `moonui_dev_be`). The physical analyzers are being repointed from moonui → elmadina. Done over the 2026-06-24 session.

**Cross-DB method (reusable for any new client):** standalone PHP scripts run as root that open TWO PDO connections (read moonui via its `.env`, write the target via its `.env` parsed with `Dotenv`), remap FKs **BY CODE** (because the two DBs' ids differ), wrap writes in a transaction, DRY-RUN first, backup first. Scripts live in `/home/moonui/*.php`. **mysqldump auth gotcha:** env/shell password passing fails (`Access denied @'localhost'`) — use a `--defaults-extra-file` (chmod 600, deleted after) with `protocol=TCP`; PDO works fine with the Dotenv-parsed creds.

## What was migrated to elmadina (verified)
| Data | Result | Note |
|---|---|---|
| Investigation catalog | **1825** + colours + panel internal sections + 971 ranges | lossless seed (below); ids NEW (by code) |
| Master data | sections/specimen(w/ color_code)/units/categories | auto-created by code |
| Patients | **11,809** | ids PRESERVED; FKs (external_lab/partner/insurance/created_by/portal tokens) nulled |
| Machines | **5** (Dymind 5600, Maglumi800 15001, MaglumiX3 15002, Udichem 15003, VITROS 15004) + 86 test-mappings | ids preserved; `connection_settings` (ports/driver) kept; investigation_id remapped by code |
| Doctors | **6** | names+specialization+inline commission; GL/HR/partner FKs nulled. Per-test commission rules (3) NOT migrated (owner: edits done manually) |
| Price lists | **9** (1 default 1810 items + 8 non-default 1199 items) | default = moonui's prices (CBC=60, TSH=100…); item investigation_id remapped by code |
| External labs | **5** (Delta DLT9594, wafaa 02062026, Atlas ex-002, Dr.amer ex-003, Zakaa ex-004) + 6 lab↔price_list links | **portal creds KEPT**; price_list_id + junction remapped by code; GL/partner FKs nulled |

## 🟢 Lossless catalog export/import (CODE CHANGE — shipped via update)
The bundled "house" catalog seed was LOSSY (master data created code-only → specimen types arrived **without colours**; panel internal sections dropped). Fixed in `LisCatalogService`:
- export now ships full `master_data` (names, **color_code**, container types, category parent-by-code) + each panel's `lab_panel_sections` + each member's group by name.
- import `seedMasterData()` upserts master data fully; 2nd pass recreates `lab_panel_sections` + sets `panel_members.section_id`; `wipeCatalog` also clears panel sections; guard forces skipExisting when not replacing.
- regenerated bundled `Modules/LIS/database/data/house_catalog.json` (scope=all, 1825). Old 150-curated kept as `.bak-150-*`.
- Commits on `hazemdev`: `bc912bf11` (lossless) — reviewed (2 HIGH addressed). Any client: Lab-Setup → "seed standard catalog (house)" now gets the full catalog with colours; `replace` (password) wipes+re-seeds.

## Middleware repoint (client-side, owner does on the Windows box)
Point the middleware at elmadina from the admin UI (`http://127.0.0.1:8765`) — no file edit needed (Save persists + reloads + re-syncs):
- **Cloud URL (`cloud.base_url`) = `https://elmadina.elbaset.com/api`**
- mode=login, email `admin@gt4.com`, password `123456789` (or paste a token)
- Test (✅) → Save → Sync → pulls the 5 elmadina machines (devices are cloud-managed/read-only).
- ⚠️ ONE instance only (Scheduled Task `MoonLisMiddleware` + run.bat = two → dup results, broken query). See [[middleware]].

## Settings / behaviour fixes on elmadina
- **`lis.allow_self_verification` = true** (was default false → blocked self-validate/approve at `/app/lab/validation`). Set via SettingsService. moonui had it =1. Definition default is `false`; group `lis_workflow`.

## 🔴 OPEN — elmadina lab GL account mis-mapping (diagnosed, NOT fixed; owner deciding)
The lab invoice/payment JEs are balanced + structurally correct, but two account settings are wrong on elmadina:
- 🔴 `lis.tax_payable_account_id = 40` → **[2105] إيرادات مقبوضة مقدماً (deferred revenue)** — WRONG. Should be **41 = [2106] ضريبة القيمة المضافة المحصّلة** (VAT collected; exactly what moonui uses). Almost certainly an off-by-one (2105 vs 2106) from the parity remap → VAT booked as deferred revenue.
- 🟠 `lis.revenue_account_id = 17` → **[44] إيرادات أخرى (other revenue)** — generic. moonui uses **[405] إيرادات المعمل**; elmadina has no lab-revenue account → options: 14=[41] إيرادات المبيعات, or create a lab-revenue account.
- `lis.method_accounts = []` empty (cash relies on `lis.cash_account_id=90` [110101] خزنه1). OK unless visa/bank methods needed.
- Correct already: AR=66 [110303] ذمم مرضى المعمل · cash=90 [110101] خزنه1 · COGS/commission/external accounts.
- **Fix when authorized:** set `lis.tax_payable_account_id=41`; decide revenue account.

## Security (on `main`, ships via the owner's release)
- **super-admin (owner) hidden from ALL user lists** — `Core/UserController::index` + `LIS/LabUserController::index` now `whereDoesntHave('roles', super-admin/web)`. Owner = `hasRole('super-admin','web')` (elmadina user 2 `owner@moonerp.app`; moonui users 10,26). Commit `ae3a07343`.
- **Lab-created users are confined to the lab** (`LabUserController`) — commit `0f5b7a990`. Two holes fixed: (1) `role` was optional on store → a roleless user has empty roles/perms = treated as **super-admin (full access)** → role is now **required**; (2) `assertRoleAssignable` only checked company → could assign `admin`/`accountant`/etc. → now only **lab-safe** roles assignable: it rejects `super-admin` and any role with a permission outside `lis.*` plus a read-only shell allowlist (`core.dashboard.view, notifications.view, company.view, branches.view, attachments.*, ai.chat, users.view` = `LabUserController::LAB_SHELL_PERMISSIONS`). On elmadina only `lab_manager` is assignable; admin/accountant/manager/hr/owner are refused. New lab roles are made lis-only via Lab → Roles (`LabRoleController`, `roleSaver->create(lisOnly:true)`).
- **Design note:** roles confine users purely by permission prefix — a lab role = `lis.*` (+ shell) so the FE permission guard + BE permission middleware block everything outside `/lab`. ALL roles on these installs are global (`company_id=NULL`); home_page/data_scope are unset → can't be used as a lab-role marker, hence the permission-set test above.

## A test request was deleted from elmadina
`LR-2026-00001` (id=1, owner's test) hard-deleted with its full tree + the 2 GL JEs (invoice JE1 + payment JE2) — no trace. Backup `elmadina_db_pre-delete-testreq.sql.gz`.

## Release path — how a fix reaches elmadina (and the 4.0.6 incident)
A client's in-app **Update** pulls the **published** release (`public_html/moonstack/versions.json` → `latest`) — which is built from **`main`**. So a fix reaches elmadina only after: commit on `hazemdev` → **merge to `main`** → **`moonstack:ship` a new version FROM main** → **promote** (`latest`=new version). Committing on hazemdev alone does NOTHING for clients.

🔴 **Incident (2026-06-25): a half-baked 4.0.6.** A `moonstack:ship 4.0.6` ran while the super-admin fix was only on `hazemdev` (NOT merged to main). Result: `4.0.6.zip` got the **LIS** half (`LabUserController`) but **NOT the Core** half (`UserController`), AND `versions.json` stayed `latest=4.0.5` (never promoted) — so it was both incomplete and not live. **Lesson:** ALWAYS merge to `main` first, ship FROM main, verify the zip actually contains the change (`unzip -p … | grep`), THEN promote. A parallel session (moonui2) shipping the same repo makes this worse — coordinate.

## STATUS (2026-06-25)
- ✅ **push + merge to `main` DONE** (FF, both repos): BE `main = ae3a07343` (super-admin Core+LIS, lossless catalog, changelogs) · FE `main = 69db677` (currency dropdown, portal fix). `main` now has the COMPLETE fixes.
- ⏳ **ONLY the release is left:** `moonstack:ship` **4.0.7 from main** + promote (`latest`=4.0.7) → it SUPERSEDES the incomplete 4.0.6 → then elmadina runs Update and gets: super-admin hidden (Core+LIS), lossless catalog seed, currency dropdown. ⚠️ `chown -R moonui:moonui` BE + `/app` before ship ([[release-chown-moonui]]).
- 🔴 **GL tax fix** on elmadina still open: `lis.tax_payable_account_id` 40→41 + revenue account decision (see OPEN section).
- ✅ **Patient-create 500 on elmadina (2026-06-25): MRN sequence collision — FIXED (owner-authorized).** Counter `last_number=7` vs max migrated MRN `11826` → duplicate-key. Bumped elmadina counter to 11826 (`scratchpad/fix_mrn.php`; next MRN = `MRN-011827`). **Permanent code self-heal also added** (BE `e2067f46c`): on a duplicate-MRN violation for an auto-generated MRN, `LabPatientController::store` jumps the counter past the table max (`SequenceService::advanceToAtLeast`) and retries (looped 3x) → any client with a behind counter auto-heals on next patient create. Root cause + toolkit fix in playbook step 3.
- 🟢 **NEW fixes committed on `hazemdev` (in [Unreleased], NOT yet released):** (a) **lab role picker confined** — Lab→Users role dropdown now lists only lab-assignable roles + super-admin hidden from lab roles screen (BE `7e48487fd`, FE `aeeeddb`; native review APPROVE); (b) **lab report printing fixed for all templates** — footer moved into the @page bottom-margin (never overlaps results/signature), blank trailing page removed, Spectrum header compacted + email removed (verified via headless-chromium renders); (c) **MRN self-heal** + (d) **generic "Histopathology Report" template now seeded by default** (subspecialty=generic, 3 dynamic sections; in `LabHistopathTemplateSeeder` which is already in `moonstack.updater.seeders` → reaches existing clients on update) (BE `e2067f46c`); (e) **Lab→Roles screen lists lab roles ONLY** — `LabRoleController::index` now ALWAYS filters to `LabRoleAssignment::isAssignable` (was: show all, hide super-admin only) so admin/accountant/manager/owner stop appearing on `/lab/roles`; the `?assignable=1` param is now a no-op; `isAssignable` hardened to reject permission-less roles (BE `e3c36ba82`, FE `4198aff`; native review APPROVE). **Study HTML:** `public_html/lab-permissions-study.html`. Verified on moonui: `/lis/roles` → 7 lab roles only; elmadina will show `lab_manager` + any new lab role. All reach elmadina with the next release.
- 📚 **How lab permissions work (canonical):** roles confine users by permission PREFIX — a "lab role" = every perm is `lis.*` or the read-only shell allowlist (`LabRoleAssignment::SHELL_PERMISSIONS`), so its users are locked inside `/lab`. All roles are GLOBAL (`company_id=NULL`); no `is_lab` marker → the permission-set test (`LabRoleAssignment::isAssignable`) IS the lab-role definition, reused by the user-picker AND the roles list. `super-admin` = `Gate::before` bypass; `owner` = all-tenant-perms. Lab→Roles create path is lis-only (`RoleSaveService::create(lisOnly:true)`); system-wide roles managed from Core→Roles.
- 🧰 `/home/moonui/*.php` migration scripts kept as the client-onboarding toolkit (see playbook).
- 👤 Owner repoints the middleware to elmadina + runs ONE instance.

## 🧰 Reusable playbook — onboard a NEW client (lab) from moonui
Same pattern used for elmadina. For client `<C>` (DB `<C>_db`, BE `/home/<C>/public_html`, company `<co>`):
1. **Backup first**, every step: mysqldump via `--defaults-extra-file` (600, TCP, delete after).
2. **Catalog (the right way):** release carries the lossless `house_catalog.json`; on the client run Lab-Setup → "seed standard catalog (house)" (or `LisCatalogService::importStandard(<co>,'house',replace=true)`). This brings 1825 tests + master data + colours + panel sections + ranges + prices. (For elmadina we ran moonui's lossless service pointed at the client DB because the client code predated the fix.)
3. **Patients / machines / doctors / price-lists / external-labs:** the `/home/moonui/migrate_*_to_elmadina.php` scripts — adapt the target `.env` path + company id. They remap FKs **BY CODE** (investigations/sections/price-lists differ in id between DBs), null cross-instance FKs (partner/GL accounts/branch/created_by), keep portal creds, preserve patient ids, and run inside a transaction with a DRY-RUN.
   - 🔴 **MUST sync sequences after bulk migration (else new records 500 on a duplicate-key).** New patients/requests/etc. get their human number from `SequenceService::generateNext(company,'lis',<entity>)` = a `sequence_counters.last_number` row (one per `sequences` row; patient seq is `module='lis' entity='patient'`, prefix `MRN`, padding 6, `reset_frequency=never`). Bulk-inserting records **does NOT advance that counter**, so it stays low (e.g. 7) while migrated MRNs occupy `MRN-000002…MRN-0NNNNN` → `generateNext` returns an already-used MRN → **`SQLSTATE 23000 Duplicate entry 'MRN-00000X' for lab_patients_mrn_unique`** (HTTP 500 on `POST /lis/patients`, in `LabPatientController::store`). **Fix = after migrating, advance the counter to ≥ max migrated number:** `UPDATE sequence_counters SET last_number = (SELECT MAX(CAST(SUBSTRING(mrn,5) AS UNSIGNED)) FROM lab_patients WHERE mrn REGEXP '^MRN-[0-9]+$') WHERE sequence_id = (SELECT id FROM sequences WHERE company_id=<co> AND module='lis' AND entity='patient');`. Do the same for any other migrated sequence-numbered entity (requests, invoices…). (elmadina hit this 2026-06-25: counter=7 vs max MRN=11826 → diagnose/fix scripts in scratchpad `diag_mrn.php`/`fix_mrn.php`.)
4. **Settings parity:** check lab GL account settings point to the client's OWN detail-leaf accounts (NOT moonui's ids, NOT headers) — esp. `lis.tax_payable_account_id` (VAT collected), `lis.revenue_account_id`, `lis.receivable_account_id`, `lis.cash_account_id`. Enable `lis.allow_self_verification` if needed.
5. **Middleware:** point `cloud.base_url` to the client API + login; ONE instance.
6. **Never** `migrate:fresh`/test on a live client DB ([[feedback_never_test_on_real_db]]); changes to client CODE go via the release/update pipeline (harness blocks direct live-source overwrites), DB-data ops are OK with a backup.

## 🔁 Cross-DB SINGLE-REQUEST migration (reusable) — 2026-06-29
Moved one B2B request **LR-2026-00400** moonui→elmadina LIVE with its full graph (20 tests + **18 released results with their values** + 4 samples/barcodes + draft B2B `external_lab_receivable` invoice) under the same B2B «dr.wafaa tolba clinic». Tool: `/home/moonui/migrate_lr00400.php` (generic: `php migrate_lr00400.php <SRC_DB> <DST_DB> <REQUEST_NUMBER> [--commit]`, default dry-run).
- **Maps every FK by STABLE KEY, not numeric id:** patient→MRN, B2B→external_lab `code`, tests→`code` (20/20), sections/specimens→`code` (HEM 1→35, COAG 7→29; EDTA 1→26, PLS 3→28), partner via the dst external_lab (moonui 46→elmadina **13**), company 4→1, branch→main.
- **🔑 GOTCHA — map ALL user FKs or the live insert 1452-fails:** missed `transitioned_by` + `assigned_to_user_id` first → clean rollback on live (txn). Fix = remap every `*_by` / `*_user_id` column to the dst importer admin (id 1). Nulled GL/machine/referral/encounter FKs (`journal_entry_id`, `machine_id`, …). Kept request_number/barcode/dates/status verbatim; invoice stays **draft** (post→GL from inside the app).
- **🔑 GOTCHA — staging passed but live failed the SAME FK:** `selmadina_stg` and `elmadina_db` have DIFFERENT user sets, so a moonui user id can exist on staging (false PASS) and not on live. → always map user FKs; don't rely on staging coincidence.
- **Safe path used:** dry-run → commit on `selmadina_stg` → **mysqldump backup** (`/home/elmadina/pre-LR00400-migration-elmadina_db-*.sql.gz`) → commit on live (txn, idempotent guard) → verify → cleaned the staging test copy. Connects as root via `/root/.my.cnf` (one PDO, cross-DB `db.table`). Reusable for any future request move. See [[staging-mirror]].
