# WP5 — Active Ingredients + Dosage Forms CRUD modules

**Repo:** BE + FE · **Branch:** `hazemdev2` · **Migration:** **YES**

## Goal

Two new company-scoped lookup modules in **`Modules/Core`**, each with a full CRUD (list, search, create, update, delete) and a reachable frontend screen:

- **`active_ingredients`** — المواد الفعّالة. A drug will link to **one or more** (the pivot is WP6's job; this WP builds the master + its screen).
- **`dosage_forms`** — الأشكال الدوائية (قرص، شراب، أمبول، كريم…).

Both are lookup tables rather than PHP enums **by decision**: the list is not closed across markets, an enum would need a code release per new form, and a table gives per-company Arabic naming.

## ⚠️ The failure mode to avoid — this is the point of the WP

This codebase already has **two orphaned lookups**: `brands` (full BE CRUD in Core, **no FE screen at all**) and `manufacturers` (full BE CRUD, but in **WebStore**, unreachable from Core — `products.manufacturer_id` exists yet is absent from Core's `ProductResource`, `StoreProductRequest` and `UpdateProductRequest`).

Do not create a third. **Both tables go in `Modules/Core`, are registered in `Modules/Core/routes/api.php`, and ship with a working FE screen reachable from the sidebar in this same WP.**

## Clone targets — read these before writing

| Layer | Copy from | Why |
|---|---|---|
| Migration + model + controller + requests + resource + factory + test | **`Brand`** — `Modules/Core/database/migrations/2026_03_26_000001_create_brands_table.php`, `Modules/Core/app/Models/Brand.php`, `Modules/Core/app/Http/Controllers/BrandController.php`, `Store/UpdateBrandRequest.php`, `BrandResource.php`, `Modules/Core/database/factories/BrandFactory.php`, `Modules/Core/tests/Feature/BrandApiTest.php` | Its column set is a 1:1 match for what we need, and its controller already has `?search=` across the name columns, `?is_active=`, a `per_page` cap, `withCount('products')` and a private `authorizeCompany()`. |
| FE component + service + NgRx slice | **`UnitGroup`** — `src/app/features/unit-groups/*`, `src/app/core/services/unit-group.service.ts`, `src/app/core/models/unit-group.model.ts`, `src/app/core/store/unit-groups/*` | Simplest complete CRUD screen in the app: `app-page-header` + `app-data-table` + inline `app-form-dialog`, service with the `listAll()` forkJoin paginator, and a full actions/reducer(+selectors at the bottom)/effects slice. |

Do **not** copy `UnitGroupController` on the BE side — it hardcodes `paginate(25)` and its resource aliases the DB `name` to a JSON `name_en`, which is legacy noise.

## Columns

Mirror `brands`: `id`, `company_id`, `code` (50, nullable), `name` (255), `name_en`, `name_ar`, `description` (text), `is_active` (bool), timestamps, softDeletes, `unique(['company_id','code'])`.
Drop `logo`. `active_ingredients` needs nothing else — per-ingredient **strength lives on the pivot** and is WP6's job.

Both models `extends BaseModel` (`app/Models/BaseModel.php`), which already brings `Auditable`, `HasArabicContent`, `SoftDeletes`, `TenantAware` — **do not hand-roll company scoping, activity log, or `getLocalizedName()`**.

## Permissions

Convention is `{module}.{kebab-plural}.{view|create|update|delete}` ⇒ `core.active-ingredients.*` and `core.dosage-forms.*`.
- Declare in `Modules/Core/app/Support/PermissionCatalog.php`.
- Grant in `Modules/Core/database/seeders/RolePermissionSeeder.php` — **three spots**, matching how Brand is handled: the master list, the role `.view` grants, and the `sales_representative` grants. Follow Brand exactly.
- FE route guard uses prefix matching, so `data: { permissions: ['core.active-ingredients'] }` covers all four verbs.
- Per-button gating with `*appCan="'core.active-ingredients.create'"` — see `unit-groups.component.html`.

## Seed the dosage forms

An empty dropdown on first run is a bad first experience. Seed the standard forms idempotently (`updateOrCreate`), bilingual: tablet/قرص · capsule/كبسولة · syrup/شراب · suspension/معلّق · injection/حقن · ampoule/أمبول · vial/فيال · cream/كريم · ointment/مرهم · gel/جل · drops/قطرة · eye drops/قطرة عين · suppository/لبوس · inhaler/بخاخة · effervescent/فوّار · sachet/كيس · spray/بخاخ · solution/محلول.
Precedent: `Modules/Core/database/seeders/DefaultUnitSeeder.php`.
**If you add a new seeder class, it must be registered in `config/moonstack.php` in BOTH the `installer.seeders` and `updater.seeders` lists** — otherwise existing clients never receive it. (Setting-definition seeders are already listed; a brand-new seeder is not.)

## FE wiring checklist (all required — a screen nobody can reach is the orphan failure)

`app/features/active-ingredients/*` and `app/features/dosage-forms/*` (ts/html/scss) · `app/core/models/*.model.ts` · `app/core/services/*.service.ts` · `app/core/store/*/{actions,reducer,effects}.ts` · `app/app.routes.ts` · `app/app.config.ts` (2 imports + `provideStore` + `provideEffects`) · `app/core/store/index.ts` re-exports · `app/layout/sidebar/sidebar.component.ts` — add both under the existing products group (where unit-groups and units already sit) · `src/assets/i18n/en.json` + `ar.json`.

i18n convention is `FEATURE.UPPER_SNAKE` (`ACTIVE_INGREDIENTS.TITLE`, `NAV.ACTIVE_INGREDIENTS`, …). The two files are **line-aligned** — add the same keys at matching positions. **Edit additively. NEVER run `git checkout`/`restore`/`stash` on either file.**

## Acceptance criteria

1. Full CRUD works for both entities over `/api/core/active-ingredients` and `/api/core/dosage-forms`, company-scoped; a user from another company gets 404/403 on a foreign row.
2. `?search=` matches across `code`, `name`, `name_en`, `name_ar`; `?is_active=` filters.
3. Permission gates enforced for all four verbs on both entities.
4. Both screens are reachable from the sidebar, list/create/edit/delete work end to end, and respect per-button permissions.
5. Dosage forms are seeded bilingually and the seeder is idempotent + registered for the fleet.
6. `ng build` green; no new BE test failures vs baseline.

## Tests

`Modules/Core/tests/Feature/ActiveIngredientApiTest.php` and `DosageFormApiTest.php`, modelled on `BrandApiTest.php` (Pest, `RefreshDatabase` — that is fine, it uses **sqlite**, never the dev DB).

⚠️ **Pest loads every test file into ONE process.** Prefix any top-level helper with its file's subject (`activeIngredientActor()`, not `actor()`). A generic name is a fatal redeclare that kills the whole suite: exit 255, zero output. Fourth occurrence in this project was fixed today.

## Migration execution

Run the new migrations on `moonui2_dev_be` in the same step the code lands (`php artisan migrate --force` with the CLI php). Timestamps must sort after all existing migrations.
⛔ Never `migrate:fresh`/`migrate:refresh`/`db:wipe` against the dev DB — not binlogged.

## Out of scope

- The product↔ingredient pivot, `is_drug`, and `product_drug_details` — that is **WP6**. Build the masters only.
- The product form's drug tab — **WP7**.
- Do not touch the POS module.

## Environment

- BE tests: `/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest --filter='ActiveIngredientApiTest|DosageFormApiTest'`.
- Baseline 174 passed / 14 pre-existing failures (all `StorefrontProductApiTest`). Green = no NEW failures.
- `pint` on touched files only; `chown moonui2:moonui2` after each edit; **moonui2 only — never `/home/moonui`**.
- Working tree is dirty by design — never revert/stash/commit what you did not write; do not commit at all.
