# Scout C — Catalog Import + Categories + Repeat-Visit Variant (م1)

Repos: BE `/home/moonui/moon-erp-be/Modules/Clinic` + `Modules/LIS`. FE `/home/moonui/public_html/moon-erp/src/app/features/clinic` (+ `features/lis`).

---

## 1. EXISTS (file:line)

### 1a. Clinic service CRUD — NO bulk/import path
- `Modules/Clinic/app/Http/Controllers/ClinicServiceController.php:20-100` — only `index/store/show/update/destroy`. No import/bulk endpoint, no CSV route. Confirmed via `grep -iE "import|bulk|csv"` on `Modules/Clinic/routes/api.php` → zero hits except a comment header `// --- WP-05 ClinicService catalog ---` (`routes/api.php:95`).
- `Modules/Clinic/app/Actions/UpsertClinicService.php:1-56` — single `create()`/`update()` only, one row at a time (allocates a code via `SequenceService`, dispatches `ClinicServiceCreated`/`Updated`).
- `Modules/Clinic/app/Http/Requests/StoreClinicServiceRequest.php:1-79` — validates one row; also has a **guard**: `lis_investigation_id`/`rad_procedure_id` may only be set when `service_type` is `lab`/`radiology` respectively, and only one of them at a time (lines 46-71). This guard must be respected by any bulk-import writer.
- `Modules/Clinic/database/seeders/ClinicDatabaseSeeder.php` — no service seeding at all (only `DoctorGradeSeeder`, `PresentHistoryQuestionSeeder`, `HistoryAnswerOptionSeeder`, `Icd10StarterSeeder`).
- `Modules/Clinic/app/Http/Controllers/RadProcedureController.php` (86 lines) — same story, plain CRUD, `grep -iE "import|bulk|csv"` → zero hits.
- FE: `features/clinic/services/clinic-service.service.ts:60-155` — `list/listAll/getById/create/update/delete` + `departments()` only. No `import`/`bulk` method. `features/clinic/services/clinic-service-list.component.ts` is a plain CRUD list (grep for `parent_service_id|repeat.?visit|follow.?up|variant|category` across all of `features/clinic/**` → **zero hits** except unrelated CSS `font-variant-numeric` and an unrelated "External-result follow-up" intents feature in `reception-order.component.ts`).

**Conclusion: there is currently NO import path of any kind for `clinic_services` — must be built from scratch. The LIS import machinery (§2) is the closest reusable pattern.**

### 1b. `clinic_services` table shape (today)
`Modules/Clinic/database/migrations/2026_06_22_010001_create_clinic_services_table.php:11-39`
```
id, company_id, code, name_ar, name_en, service_type(20),
department_id (FK → departments, nullable),
base_price, requires_doctor, default_duration,
lis_investigation_id (soft FK, no DB constraint — LIS is independent),
rad_procedure_id (soft FK, no DB constraint),
sbscs_code, product_id (FK → products),
is_active, sort_order, created_by, updated_by, timestamps, softDeletes
unique(company_id, code)   ← already company-scoped, good pattern to copy
```
`Modules/Clinic/app/Models/ClinicService.php:24-42` fillable mirrors the above exactly. **No `category_id`/`category` column. No `parent_service_id` column.**

`department_id` here is **not a service category** — it's a `belongsTo(Modules\HRM\Models\Department)` (org department, enum `Clinical`/`Administrative` — `Modules/Clinic/app/Enums/DepartmentType.php`), also reused as one axis of `clinic_service_prices.scope='department'` tiered pricing (`Modules/Clinic/database/migrations/2026_06_22_020001_create_clinic_service_prices_table.php:11-25`). Don't confuse it with a lab/rad-style category grouping — it answers "which org unit owns this service", not "which section of the pick-grid does this test belong to".

### 1c. `rad_procedures` table shape (today)
`Modules/Clinic/app/Models/RadProcedure.php:1-56` + its migration (`Modules/Clinic/database/migrations/..._create_rad_procedures_table.php`):
```
id, company_id, code(nullable!), name_ar, name_en, modality, body_part,
price, is_active, created_by, updated_by, timestamps, softDeletes
```
No unique constraint on `(company_id, code)` at all — comment explicitly says "SQLite doesn't support partial indexes, so avoid unique constraint... enforce in application layer if needed" — **not currently enforced anywhere found**. No category column either. `RadProcedure` lives INSIDE `Modules/Clinic` (folded in per an architecture decision noted in the migration docblock: "no separate Modules/Radiology"), mirrors `LabInvestigation`'s shape as the pricing/definition master, money stays on `ServiceOrderLine`.

---

## 2. Reusable LIS-import pieces for a clinic one-click lab/rad import

The LIS "Lab Setup wizard" (KB `topics/lis-setup-screen.md`) already solved almost exactly this problem for the LIS module itself — importing a curated name+category catalog without touching machines. This is the template to copy/adapt for Clinic standalone mode.

### 2a. Backend architecture (fully mapped, verified live)
| File | Role | Reusability for Clinic |
|---|---|---|
| `Modules/LIS/app/Http/Controllers/LisCatalogController.php:1-115` | `GET investigations/export?scope=`, `POST investigations/import-standard` (`source=house\|nafis`, `skip_existing`, `with_prices`, `replace`+password-gated wipe) | **Direct pattern to clone** as `ClinicCatalogController` with `export`/`importStandard` for `clinic_services`. The `replace`+`Hash::check` password gate (lines 86-91) and the `QueryException`→friendly-422 catch (lines 101-106, catches FK-blocked wipes) are both worth copying verbatim. |
| `Modules/LIS/app/Services/LisCatalogService.php:1-878` | Export: walks `LabInvestigation` with `CORE_FIELDS` (name/code/etc, **by value**) + `MASTER_FIELDS` (sections/specimen_types/**categories**/units, **by code**, self-referential `parent_code` for categories at lines 371-393) → portable code-referenced JSON. Import: `firstOrCreate` master data by `(company_id, code)` (`resolveMaster`, lines 808-829), then bulk-creates rows, all inside one `DB::transaction`. | **This is the exact machinery needed.** For Clinic: `CORE_FIELDS` would map to `ClinicService`'s fillable (code/name_ar/name_en/service_type/base_price/…), and a new `clinic_service_categories` `MASTER_FIELDS`-style entry (see §3) would resolve by code with `firstOrCreate`, mirroring `resolveMaster()` exactly. **Machine-specific stuff (sections tied to analyzers, specimen types, units) is NOT needed for Clinic standalone import — only categories + names + prices are relevant**, so the port is actually simpler than the LIS original (fewer MASTER_FIELDS tables, no panel-member 2nd-pass, no formula-dependency 3rd-pass). |
| `Modules/LIS/database/data/house_catalog.json` (4.2 MB, 1825+ investigations, 20 panels, prices) | Bundled JSON snapshot, regenerable via `GET .../export?scope=house` | For Clinic: would need an **equivalent bundled JSON of lab-TEST-NAMES + rad-PROCEDURE-NAMES + categories only** (no ranges/panels/formulas — those stay LIS-only concepts). Could literally be **derived from the same `house_catalog.json`** — strip everything except `code, name_ar/en, category_code` for lab, and a small curated rad-procedure list (no equivalent bundled data exists for rad yet — would need to be authored, since `rad_procedures` has no seeder at all). |
| `Modules/LIS/database/seeders/NafisCatalogSeeder.php` — `seedForCompany()` public, idempotent by `sbscs_code`, loads `nafis_catalog.csv` (1666) + `nafis_links.csv` (231) | The "national catalog" alternative source | Names+categories only (no ranges/pricing needed) could be extracted the same way for a clinic-standalone "NAFIS names only" import, if desired — lower priority than the house-catalog port. |
| `Modules/LIS/app/Jobs/ImportInvestigationsJob.php` | CSV import (`fgetcsv`-based, existing `POST /lis/investigations/import`) | Reusable **pattern** (not code) for a "CSV import" fallback option in Clinic's wizard, matching KB's noted gotcha: CSV-only (no Excel parser installed), so keep parity — restrict to `.csv` from day one. |

### 2b. Frontend architecture (fully mapped)
| File | Role | Reusability |
|---|---|---|
| `features/lis/lab-setup/lab-setup-wizard.component.ts:49` (`type InvestigationImportMode = 'house'\|'nafis'\|'csv'\|'skip'`), `:564-641` (`setImportMode`, `onCsvSelected`, `runImport`) | Step-4 "Investigations Import" logic: radio-mode switch, CSV file picker, `replace`+password checkbox (house-only), calls `importStandard()` or `importCsv()`, shows result counts | **Directly portable UI pattern** for a Clinic "Import Services" screen/dialog — same 3-source radio (house/nafis-equivalent/csv) + skip option. Given Clinic м1 wants "ONE-CLICK", the simplest MVP is just the `house` (curated) mode — drop `nafis`/`csv` initially and add later if requested. |
| `core/services/lis-investigation.service.ts` — `importStandard`, `importCsv`, `exportCatalog` methods | Typed HTTP wrappers | Pattern to mirror in a new `clinic-catalog-import.service.ts`. |
| `core/services/lis-price-list.service.ts` — `bulkSetup` | Bulk pricing modes (`from_house`/`flat`/`percent`/`keep`) | Optional reuse if م1 also wants bulk-price-on-import for clinic services (not explicitly required by the task, but `clinic_service_prices` already supports a `base` scope so a "set base_price from import" step is trivial — it's literally the `base_price` column on `clinic_services` itself, no separate price-list table needed since Clinic's pricing model is simpler than LIS's). |
| `features/lis/investigation-categories/lis-investigation-categories.component.ts` (219 lines, per `features/lis/CLAUDE.md`) — Category CRUD with parent hierarchy | **Direct UI pattern to clone** for a `clinic-service-categories` CRUD screen (see §3). |

### 2c. What is explicitly NOT reusable / must be dropped for Clinic standalone import
- Panel members / panel sections (2nd pass in `importHouse`, lines 552-605) — clinic services have no panel concept.
- Normal ranges (`RANGE_FIELDS`, lines 70-75) — lab-result-specific, irrelevant to a service catalog entry.
- Formula dependencies (3rd pass, lines 607-628) — LIS calculation engine only.
- Sections/specimen-types/units master data — analyzer/sample-workflow concepts that don't exist in Clinic; only **categories** carry over.
- The LIS `code` columns' **GLOBAL unique** limitation (KB warns: `lab_investigations.code` etc. are globally unique, not `(company_id, code)`, so cross-company import on the same DB collides) — **Clinic's `clinic_services` table already avoids this mistake** (`unique(['company_id','code'])`, migration line 34). Any new `clinic_service_categories` table must copy Clinic's pattern, NOT LIS's `lab_investigation_categories.code->unique()` global-unique mistake (see §3, flagged again below).

---

## 3. Categories + `parent_service_id` gaps + smallest changes

### 3a. Categories — confirmed GAP, no column/table exists
- `clinic_services` has no `category_id` and no `clinic_service_categories` table (grep across `Modules/Clinic/database/migrations` for `categor*` → zero results, only LIS has `lab_investigation_categories`).
- **Existing pattern to mirror** (best precedent in the codebase): `Modules/LIS/database/migrations/2026_03_01_000003_create_lab_investigation_categories_table.php`:
  ```
  id, company_id, name, name_ar, code (⚠️ globally unique — LIS's mistake, see below),
  description, parent_id (self-FK, nullOnDelete), is_active, sort_order,
  created_by, updated_by, timestamps, softDeletes
  ```
  This is exactly the shape needed for `clinic_service_categories`, **except** the `code` column must be `unique(['company_id','code'])` like `clinic_services` already does — LIS's `->unique()` bare/global unique on `code` is a known limitation flagged in the KB (`topics/lis-setup-screen.md` §"Known limitation") and should NOT be repeated.
- **Smallest change (flagged migration):**
  1. New migration: `create_clinic_service_categories_table` — `id, company_id (FK companies), code, name_ar, name_en, description(nullable), parent_id (self-FK companies-scoped, nullOnDelete), is_active, sort_order, created_by, updated_by, timestamps, softDeletes`, `unique(['company_id','code'])`, `index(['company_id','is_active'])`.
  2. New migration: `add_category_id_to_clinic_services_table` — `$table->foreignId('category_id')->nullable()->after('service_type')->constrained('clinic_service_categories')->nullOnDelete();` + index `['company_id','category_id']`.
  3. New model `ClinicServiceCategory` (mirrors `LisInvestigationCategory`'s parent/children relations) + add `category()` belongsTo on `ClinicService` + add `category_id` to `ClinicService::$fillable` + `StoreClinicServiceRequest`/`UpdateClinicServiceRequest` validation (`Rule::exists('clinic_service_categories','id')->where('company_id', ...)`).
  4. FE: new `ClinicServiceCategory` model/service (clone `LisInvestigationCategoryService`), new small CRUD screen (clone `lis-investigation-categories.component.ts`, 219 lines — small effort), add `category_id` filter/column to `clinic-service-list.component.ts` + form.
  - This is additive-only (new table + one nullable FK column) — no data migration risk, no breaking change to existing `clinic_services` rows (they'll just have `category_id=NULL` until categorized, either manually or via the one-click import in §2 which can populate categories from `MASTER_FIELDS['categories']`-equivalent data alongside the imported services).

### 3b. `parent_service_id` / repeat-visit variant — confirmed GAP, no column/concept exists
- Grepped `ClinicService.php` fillable (§1b) — no `parent_service_id`. Grepped entire `Modules/Clinic` for `parent_service|variant|repeat.?visit|follow.?up.*service` → only unrelated hits (`ContractServicePriceChanged` event, `contractPrices()` relation — pricing-per-contract, not a service-variant concept).
- Grepped FE `features/clinic/**` for the same terms (§1a) → zero hits; the only "follow-up" concept in the whole Clinic FE is an unrelated one — external-lab-result follow-up banner in `reception-order.component.ts:148,497` (waiting-for-external-results UI, nothing to do with pricing/variant services).
- **Smallest change (flagged migration):**
  1. New migration: `add_parent_service_id_to_clinic_services_table` — `$table->foreignId('parent_service_id')->nullable()->after('id')->constrained('clinic_services')->nullOnDelete();` + index `['company_id','parent_service_id']`. Self-referential FK on the same table (same pattern already used for `LabInvestigationCategory.parent_id` and would-be `ClinicServiceCategory.parent_id` above — well-precedented in this codebase).
  2. Add `parent_service_id` to `ClinicService::$fillable` + a `parentService()` belongsTo / `variants()` hasMany relation pair (mirrors `department()`/`product()` pattern already on the model).
  3. Validation: `StoreClinicServiceRequest`/`UpdateClinicServiceRequest` — add `'parent_service_id' => ['nullable','integer', Rule::exists('clinic_services','id')->where('company_id', $this->user()->company_id)->whereNull('deleted_at')]`, plus an app-level guard (mirroring the existing `guardBridgeIds()` pattern at lines 40-77) that a service cannot be its own parent, and probably that `parent_service_id` should only be set when `service_type` matches the parent's `service_type` (keeps a "follow-up consultation" chained only to a "consultation", not cross-type) — this is a judgment call for the owner/architect to confirm during actual implementation, not decided here.
  4. FE: add a `parent_service_id` picker (searchable dropdown filtered to same `service_type`, excluding self) to the service form, and a small badge/indicator on `clinic-service-list.component.ts` rows that have a parent ("Variant of: X") or that have children ("has N follow-up variants").
  - Also additive-only, no breaking change. No pricing/cascade logic exists yet — cheaper "follow-up" pricing is presumably just `base_price` set independently on the variant row (`clinic_service_prices` already supports per-service overrides at `base`/`department`/`doctor` scope, so no schema change needed there — the variant is simply a second `clinic_services` row with its own `base_price` and a `parent_service_id` pointer back to the original for FE grouping/UX purposes, e.g. "Book follow-up" quick-action next to a consultation service).

### 3c. Combined smallest migration set for م1 (summary, 2 new tables/columns worth flagging)
1. `create_clinic_service_categories_table` (new table, additive)
2. `add_category_id_to_clinic_services_table` (new nullable FK column, additive)
3. `add_parent_service_id_to_clinic_services_table` (new nullable self-FK column, additive)
4. (Optional, not required by schema but needed for the "ONE-CLICK import" UX) a bundled `clinic_house_catalog.json`-equivalent data file — **authoring/curation work, not a migration** — derived from `Modules/LIS/database/data/house_catalog.json` for lab test names+categories, plus a net-new curated rad-procedure list (doesn't exist anywhere yet in any form — no rad seeder, no rad catalog JSON found anywhere in the repo).

None of these migrations touch existing columns or existing data — all are additive (new table / new nullable FK), so they carry no destructive-migration risk. No code changes were made; this is a read-only map for planning.
