---
title: Formula-test dependencies (portability) + result decimal places
slug: lis-formula-dependencies
status: shipped
updated: 2026-07-01
related:
  - elmadina-onboarding
  - lis-setup-screen
plans:
  - https://moonui.elbaset.com/investigation-decimals-study.html
---

# Editing calculated (formula) tests + per-test decimal places

Two fixes from the 2026-07-01 elmadina report «مش بعرف أعدّل التحاليل» (started as a decimals question, the screenshot revealed the real blocker). BE `679b25aa0` (+ reconciliation `8768e26a5` + systematic LDL adoption `8cd6b06cb`) + FE `5ecd13d`, on `main`, ⏳ owner releases. **5.0.3 already released + updated to staging/elmadina (has fix #1 & #2); the reconciliation + LDL-adoption migration ship in the NEXT release (≥5.0.4).** Study: `public_html/investigation-decimals-study.html`.

## 1. 🔴 Formula-test editing was universally blocked (the real bug)
**Symptom:** editing ANY formula test (MCH/MCV/LDL…) on elmadina → «تم الحفظ» never; error `The selected formula_dependencies.0/1 is invalid`. So «can't edit the decimal» = the whole SAVE was blocked.
**Root cause:** `lab_investigations.formula_dependencies` = a JSON list of **investigation IDs** (the formula's inputs). Onboarding copied them **verbatim from the source install** — but IDs are NOT portable. On elmadina ids `[2,22,23]` don't exist (real HGB=2612, RBC=3257, HCT=2609) → the request's `exists` rule + the `ValidatesFormulaCohesion` after-validator both reject → save 422s. Auto-calc still worked (the engine `LabResultService::evaluateFormula` resolves by **code**, not IDs) — only the metadata IDs were broken. 9/10 elmadina formulas fully derivable from codes; only **LDL** used an alias `TC` (for CHOL).
**Fix (the deps are DERIVABLE from the formula):**
- NEW `Modules/LIS/app/Support/FormulaDependencies::resolve($formula,$companyId)` — dep IDs = investigations whose **code** appears as a formula token (mirrors `evaluateFormula` scan: strip `{}`, `/[A-Za-z][A-Za-z0-9_]*/`, exclude `pow/min/max/exp/log/ln/abs/sqrt/round/ifg/age`). **Matched-code-ONLY** — never trusts client/catalog IDs (a stale ID that collides with a *different* local test must not be linked — Codex HIGH).
- Derivation runs in the request **`prepareForValidation()`** (`ValidatesFormulaCohesion::prepareFormulaDependencies`) so BOTH the cohesion after-validator AND persistence use the sanitized list; `exists` rule relaxed to plain `integer`. Non-formula tests → deps cleared. Pure renames untouched (`hasAny` guard preserves the cohesion "skip unrelated edits").
- **Repair migration** `2026_07_01_130000_repair_formula_dependencies.php` (ship-with-update, `withinTransaction=false`, changed-only) fixes existing installs.
- **Catalog import 3rd pass** derives after all rows exist; `formula_dependencies` removed from `CORE_FIELDS` (no longer carried); the one catalog alias fixed at source: **house_catalog.json LDL formula `TC` → `CHOL`** so it's fully derivable on fresh installs.
- **Reviews:** Codex (read-only — WORKS here now via `codex exec --sandbox read-only`, despite `max_user_namespaces=0`) REQUEST-CHANGES → all applied (matched-only, prepareForValidation, non-formula clear, import changed-check). Native reviewer WARNING → applied. **⚠️ native reviewer polluted `moonui_dev_be` via tinker (company id 6 "Ruecker Ltd" + inv 3937-3940) — I deleted them.** +7 Pest (`FormulaDependenciesTest`) incl. stale-save-repairs, collision-no-422, non-formula-clear; `LabFormulaTubeTest` foreign-dep test updated (foreign dep now DROPPED not 422'd).
- **✅ LDL alias now fixed SYSTEMATICALLY (2026-07-01, not a one-off).** The stale-dep repair (`130000`) only touches rows with STALE deps; LDL's deps repair to valid `[HDL,TG]` so it's skipped, and `TC` (alias for `CHOL`) stays unbound → `evaluateFormula` returns null → **LDL silently stops auto-calculating** (confirmed on **s-elmadina staging @ 5.0.3** during verification: formula still `TC - HDL - (TG / 5)`, deps `[HDL,TG]`). Owner chose the systematic route («تتظبط لما يجيله الابديت … علشان تتظبط في بقية النسخ كلها»). NEW migration `2026_07_02_090000_adopt_catalog_formula_for_aliased_tests.php` adopts the catalog's corrected formula (`TC`→`CHOL`) for a test **only when it genuinely fails to compute AND the catalog resolves it** — gated on `FormulaDependencies::isFullyResolvable($formula,$deps,$company)` which **mirrors `evaluateFormula`'s bind rule exactly** (unmatched alias tokens must map 1:1 by count to still-valid deps). So a **WORKING authored alias** (`TC` already bound to `CHOL` via deps) is **never clobbered**, and a custom formula with no catalog counterpart is left alone. Idempotent; reaches every install on next update. +3 Pest. Codex APPROVE.
- **Reconciliation (Codex rounds 2 & 3):** `resolve()` now preserves the caller's still-valid alias deps ONLY when they map **1:1 by count** to the formula's unmatched tokens AND **in caller order** — mirroring the engine's positional bind (`token[i]↔dep[i]`); stale/foreign ids never survive; ambiguous extras drop to matched-only. The save path passes current deps as keep; `130000` gained a `hasStaleDeps` gate so a working/custom formula is never rewritten, and passes current deps to recover a still-valid alias binding.
- **Lesson:** never carry install-specific IDs across installs — derive from a portable key (codes). Applies to any ID-list field in onboarding. And when auto-repairing derived data, gate on the **runtime's actual success condition** (does it compute?), not a proxy (has an unmatched token?) — else you clobber working authored data.

## 2. Per-test decimal places now apply consistently
**Was:** `decimal_places` applied 3 different ways — results-entry `toFixed(exact)`, validation NONE, report `toFixed(minimum, never-truncate)` → a test set to 1 decimal could still print `5.11`. Editable but capped at 4.
**Now (owner spec «رقم/رقمين/أي عدد/أو أسيبه بدون تعديل»):** ONE shared `core/utils/lis-result-format.ts::formatResultValue(value,dp,resultType)` — **dp>0 → round to EXACTLY dp**; **dp=0 → leave as entered** (the safe "بدون تعديل" — never rounds an unconfigured/dp-0-default test to integer); text/qualitative (`Negative`, `>100`) pass through. Applied in the **printed report** (was minimum), **results-entry initial value** (fixes dp=0→integer bug), and **validation/worklist read-only** value (new `ResultRow.decimalPlaces` + `displayValue()`). **Display only — stored value keeps full precision.** Cap raised **4→6**; hint updated. FE-only.

## Key files
BE: `Modules/LIS/app/Support/FormulaDependencies.php` · `Http/Requests/Concerns/ValidatesFormulaCohesion.php` (prepareFormulaDependencies) · `Http/Requests/{Store,Update}LabInvestigationRequest.php` (relaxed rule + prepareForValidation) · `app/Services/LisCatalogService.php` (CORE_FIELDS − deps, 3rd pass) · `database/migrations/2026_07_01_130000_*` · `database/data/house_catalog.json` (LDL). FE: `core/utils/lis-result-format.ts` · `core/services/lis-print-report.service.ts` · `features/lis/results/*` · `features/lis/validation-worklist/*` · `features/lis/investigations/*.html`.
