---
name: lis-sample-generation
description: How lab_samples (tubes/barcodes) are created in LIS — the trigger is the FE wizard (not request creation), and the barcode-print gate only checks that sample rows exist. Why some pending requests print barcodes and others fail "No collected samples".
updated: 2026-06-18
---

# LIS Sample Generation & the Barcode-Print Gate

## ✅ FIXED (2026-06-18) — root cause confirmed + 4 changes shipped
**Root cause (confirmed on data):** the FE fire-and-forget tube-creation call failed for **branch-less users**. `LR-00356/00357` were created by **Aghsan adel (user 19)** who has **NO branch** → requests stamped `branch_id=NULL` → the FE post-save steps (sample-gen call + payment routing, both branch-dependent) failed → 0 samples → "No collected samples". `LR-00358` was created by Ahmed (user 3, has branches 5/8/13) → worked. Payment does NOT gate sample-gen (`recordPayment` always calls `onOrderSuccess`); the missing branch is the common root of both "unpaid" and "no samples".
**Fixes (BE in live tree + FE rebuilt to /app; uncommitted → ship next build):**
1. **Server-side generation** — `LabSampleService::generateForRequest()` (parent tube + `aliquot()` section children + external tubes, idempotent) called from `CreateLabRequest` before the final load → every request gets tubes regardless of client/branch/permission. `LabSampleController::store` made idempotent (returns existing tubes for the request) so the FE's `onOrderSuccess` call can't duplicate. **Verified:** backfilled the 2 stuck requests → 4 samples each.
2. **Branch fallback** — `User::primaryBranch()` now falls back to the company **main branch** (`is_main`, else first branch) when the user has none → no more null `branch_id`. Verified: Aghsan → `#8 Elrayad is_main=1`.
3. **User-add requires a branch** — `StoreUserRequest.branch_ids` `nullable`→`required|array|min:1`; FE `users.component` form `branch_ids` gets `Validators.required`.
4. **Install seeds a main branch** — `Installer::createCompanyAndAdmin()` find-or-creates the company's main branch + assigns the admin (every install has ≥1 branch from day one).
5. **Barcode lumps per USER (data-scope visibility)** — separate from the no-samples bug. `aghsan@a.com` (user 19) has the **`own` data scope** (sees only `created_by = self`). `LabSampleService::aliquot()` created the section CHILD tubes with `created_by = auth()->id()`, which is **NULL when aliquot runs without an auth user** (server-side `generateForRequest` / backfill / job). So children had `created_by=NULL` → invisible to an `own`-scoped user → her barcode print saw only the parent → **lumped every section onto one label**; admin (`all` scope) saw the children → split. **Fix:** `aliquot()` → `created_by = auth()->id() ?? $sample->created_by` (children inherit the parent's creator) + a one-time data fix (`UPDATE lab_samples c JOIN lab_samples p ON p.id=c.parent_id SET c.created_by=p.created_by WHERE c.created_by IS NULL`). NOTE: the barcode "rules" (per specimen×section split, what shows, format) all live in ONE config (`BarcodeConfig` in lab-info, applied uniformly by `printLabelsV2`) — the lumping was NEVER a settings issue; it was the child samples being invisible to the printing user.

Data note: the moonui company has TWO `is_main=1` branches (#2, #8) — should be one. All logged in `docs/moonstack/CHANGELOG.md`.

## TL;DR
`lab_samples` rows are **NOT auto-created when a `lab_request` is created.** Sample generation is a
**separate, front-end-triggered** step (fire-and-forget in the browser). If that step never runs (alt
creation path, browser error/navigation, missing permission), the request is committed `pending` with
**zero samples**, and barcode print fails with **"No collected samples"** (`LIS.SAMPLES.NO_SAMPLES`).

## What creates `lab_samples` rows (Backend, explicit)
- **Parent tube** — `LabSampleController::store()`
  - `Modules/LIS/app/Http/Controllers/LabSampleController.php:178` (auto_split parent) / `:207` (plain).
  - With `auto_split=true`: one pending parent (`parent_id`/`lab_section_id` NULL) + barcode via
    `LabSampleService::generateBarcode()` (`LabSampleService.php:726`). Then `SampleInvestigationService::createForSample()` fills the per-test pivot.
- **Child (sectional) tubes** — `LabSampleService::aliquot()`
  - `Modules/LIS/app/Services/LabSampleService.php:533-700` (child create at `:635`).
  - Groups the request's billable investigations by `(lab_section_id, specimen_type_id)`, one child per group.
  - Child barcode = parent barcode + section suffix.
- **Backend does NOT auto-generate samples on request creation:**
  - `Modules/LIS/app/Actions/CreateLabRequest.php` has **zero** `LabSample` refs (only optional `auto_generate_invoice`, lines 294-300).
  - `Modules/LIS/app/Providers/EventServiceProvider.php` — no listener on request-created; all sample listeners fire *after* a sample exists.

## What TRIGGERS it (the crux)
The **active** request wizard is **request-wizard-v2** (wired in `app.routes.ts:204-205` and
`lis-standalone.routes.ts:142-152`; v1 `lis-request-wizard` is no longer routed).
After a successful order, `RequestWizardV2Component.onOrderSuccess()`
(`src/app/features/lis/request-wizard-v2/request-wizard-v2.component.ts:1714-1779`) calls:
1. `sampleSvc.create({ lab_request_id, patient_id, auto_split:true, investigation_ids })` (line 1737) → parent.
2. `sampleSvc.aliquot(s.id)` per non-external parent → children.

This chain is **decoupled from the committed request** and **fire-and-forget**: the error branch still
`navigateAfterSuccess()`. So the request can be saved with no tubes.

## The barcode-print gate (only checks row existence)
`printLabels()` in `src/app/features/lis/requests/lis-requests.component.ts:994-1005`:
- `sampleService.listByFilter({ lab_request_id, per_page:50 })` →
  if `allSamples.length === 0` → warn toast `LIS.SAMPLES.NO_SAMPLES` and `return`. It **reads only**, never creates.
- Gate keys on **row existence only** — NOT on `status` or `collected_at`. "collected" in the message is a misnomer.

## Lifecycle
1. Create request → `CreateLabRequest` (request + optional invoice). **No samples.**
2. Generate tubes (FE `onOrderSuccess`) → `POST /lis/samples (auto_split)` + `aliquot`. Rows written `status=pending, collected_at=NULL`.
3. Print barcode → `printLabels()` reads existing rows.
4. Collect (later, independent) → `LabSampleService::collect()` flips `status→Collected`, stamps `collected_at`.

**Normal intended state:** a `pending` request whose tubes exist but are not yet collected
(`collected_at=NULL`). A pending request with NO tubes is the anomaly.

## Evidence case (moonui_dev_be, 2026-06-18)
| req | created_by | n investigations | n lab_samples | barcode print |
|-----|-----------|------------------|---------------|---------------|
| LR-2026-00356 (id46) | 19 Aghsan adel | 24 | 0 | FAIL |
| LR-2026-00357 (id47) | 19 Aghsan adel | 8  | 0 | FAIL |
| LR-2026-00358 (id48) | 3 Ahmed | 3 | 2 (parent 93 + child 94/sec5) | WORKS |
- All three `pending`, `walk_in`, **all investigations fully configured** (specimen + section non-null on all).
  Failing ones are even richer in sections → config is NOT the cause.
- 00358 samples: parent `26061800128` + child `260618001285`, both `collected_at=NULL`, created 3s after the request.
- with-trashed = 0 for 46/47 → samples never created (not deleted).

## No setting governs this
No `lis.auto_generate_samples` (or equivalent) exists. `lis.auto_generate_invoice` = invoice only.
`lis.require_payment_before_sample` gates *collection*, not generation.

## Fix direction (recommended)
Move sample generation to the **Backend on request creation** (in `CreateLabRequest` or an event/listener,
same transaction), reusing `LabSampleService::aliquot()`. Guarantees tubes exist for every request
regardless of creation path / browser failure; keeps `printLabels()` a pure reader.
Immediate workaround: run `POST /lis/samples (auto_split)` + `aliquot` for the stuck requests.

## Full report
`knowledge-base/plans/lab-barcode-no-samples-investigation.html`
