# Module Authoring Guide

Every feature in this system is a module. This is the contract each one follows.

Read this before writing a module. The rules here are enforced by the build
(`composer gates`), so ignoring them fails CI rather than surviving until review.

---

## 1. Generate the skeleton

```bash
php artisan module:make Pharmacy --entity=Drug
```

This is not a convenience — it is the contract made executable. A generated
module already has its service, repository, DTO, policy, form requests,
migration, routes, translations, views and a passing test. The cheapest way to
add a module is therefore also the architecturally correct one, which is the
only reliable defence against a rushed module that puts business logic in a
controller.

Then:

```bash
php artisan module:sync     # register it, run its install hook
php artisan migrate         # create its tables
php artisan module:list     # confirm it boots
```

---

## 2. Anatomy

```
Modules/Pharmacy/
├── module.json                     manifest — the module's public declaration
├── Providers/                      bindings, policies, commands
├── Contracts/                      PUBLIC API: interfaces only
├── Http/{Controllers,Requests,Resources,Middleware}/
├── Services/                       all business rules
├── Actions/                        single-purpose use cases (optional)
├── Repositories/                   persistence for aggregate roots
├── Queries/                        read-optimised queries (DataTables, reports)
├── Support/                        specifications shared by validation + services
├── DTOs/                           what crosses the service boundary
├── Models/                         data mapping only
├── Policies/
├── Events/ Listeners/ Jobs/ Notifications/
├── Enums/ Exceptions/
├── Database/{Migrations,Seeders,Factories}/
├── Resources/{views,lang/{en,ar},assets}/
├── Routes/{web.php,public.php,api.php,console.php}
├── Config/pharmacy.php
└── Tests/{Feature,Unit}/
```

The service provider extends `App\Modules\BaseModuleServiceProvider`, which
wires config, views, translations, migrations, routes, policies and commands
from these conventions. A module provider is usually just its bindings.

---

## 3. The manifest

```json
{
    "key": "pharmacy",
    "name": "Pharmacy",
    "version": "1.0.0",
    "namespace": "Modules\\Pharmacy",
    "providers": ["Modules\\Pharmacy\\Providers\\PharmacyServiceProvider"],
    "requires": ["patients", "billing"],
    "feature": "pharmacy",
    "core": false,
    "order": 200,
    "permissions": ["pharmacy.view", "pharmacy.dispense"],
    "menu": [{ "label": "Pharmacy", "route": "pharmacy.index", "icon": "bi-capsule", "permission": "pharmacy.view", "order": 200 }]
}
```

| Field | Meaning |
|---|---|
| `key` | lower_snake_case. Namespace for views (`pharmacy::index`), translations and config |
| `requires` | Module keys that must boot first. A missing one blocks this module with a visible reason, never a 500 |
| `feature` | Licence feature key. `null` means always available |
| `core` | Cannot be disabled, never licence-gated |
| `order` | Boot and menu ordering among independent modules |
| `menu` | Contributed navigation. Filtered by module state, route existence and permission — an item that renders is guaranteed clickable |

Adding a module adds its menu entry with no edit to any shared layout file. If
installing a module required someone to open `sidebar.blade.php`, modules would
not be independently installable.

---

## 4. Layer rules

```
Controller → FormRequest → DTO → Service → Repository → Model
                                     ↓
                                  Events
```

**Controller** — max ~15 lines per action. Resolve dependencies, authorize, map
the validated request to a DTO, call one service method, return a response.
No `if` on business state, no Eloquent, no `DB::`, no transactions.

**FormRequest** — every write endpoint has one. `authorize()` delegates to a
Policy; never an inline role-name check. Controllers consume `validated()`,
never `all()`.

**DTO** — `final readonly`, extends `App\Foundation\Dto\Dto`, with a named
constructor per source. Services accept DTOs, never `array $data` and never a
Request. This is what lets a web controller, an API controller, an Artisan
command and a queued job drive the same use case identically — and it is why the
future mobile app will not need business logic rewritten.

**Service** — one public method per use case. Owns the transaction boundary
(`use InteractsWithTransactions`), enforces invariants, throws domain
exceptions, emits events. Stateless, depends on interfaces.

**Repository** — one per aggregate root, not per table. Extends
`BaseRepository`, implements a `Contracts\` interface. Adds domain-meaningful
finders (`findActiveByPhone`), never a `where()` passthrough.

**Query object** — for DataTables feeds, dashboards and reports. Extends
`App\Foundation\Query\QueryObject` with an allow-list of sortable columns,
because sort columns arrive from the request. Do not force a twelve-filter
listing screen through a repository; that is where layered architectures rot.

**Support** — a specification both the validation layer and the service layer
need: a password policy, a pricing rule, a scheduling constraint. Not a use
case, so not a Service — a form request may consult one, and a form request has
no business importing from `Services/`. The build enforces that.

**Model** — casts, relations, scopes, `$fillable`. No business rules, no
notifications from model events, no static finders used as a service layer.

Columns that must never be set from a request — `password_changed_at`,
`failed_login_count`, `locked_until` — stay out of `$fillable` entirely. Where a
service legitimately needs to write one, the repository exposes a named method
(`replacePassword`, `clearLockout`) rather than a generic `forceFill` escape
hatch a later refactor could reach for.

---

## 5. Talking to other modules

Exactly five ways, and the build fails on anything else:

1. **Domain events** — the default.
   `Prescriptions` dispatches `PrescriptionIssued`; `Pharmacy` listens.
   `Prescriptions` does not know `Pharmacy` exists. This is how Lab, Accounting,
   Telemedicine and AI attach later without editing a single existing module.

2. **Published contracts** — only when a synchronous return value is needed.
   Depend on `Modules\Pharmacy\Contracts\StockServiceInterface`, resolved from
   the container.

3. **Directories** — for *reading* another module's data. A directory takes ids
   and returns summary DTOs, with a batch method so the fast path is also the
   correct one ([ADR 0012](adr/0012-cross-module-directories.md)).

4. **Screen slots** — for putting a panel on another module's screen. The host
   declares a named region; you register a view into it from your service
   provider, and it renders only while your module is installed, enabled and
   licensed ([ADR 0015](adr/0015-screen-slots-for-module-panels.md)):

   ```php
   // In your service provider's bootModule()
   $this->app->make(SlotRegistry::class)->register(
       slot: 'encounter.sidebar',
       view: 'yourmodule::partials.panel',
       order: 40,                       // gaps of ten, like the menu
       permission: 'yourmodule.view',
   );

   View::composer('yourmodule::partials.panel', YourPanelComposer::class);
   ```

   Slots pass **scalars**, never models — a panel that accepts an `Encounter`
   has imported another module's Eloquent class and defeated the point. Load
   what the panel needs in a view composer under `Http/ViewComposers/`, which
   has controller-level privileges and the same prohibition on touching Eloquent
   directly.

   When you host a screen others may want to extend, declare the slot yourself:

   ```blade
   <x-module-slot name="encounter.sidebar" :data="['encounterId' => $encounter->id]" />
   ```

5. **Ports in core** — for a concern another module owns but yours cannot depend
   on. `App\Foundation\Billing\ChargeCollector` is the first: a laboratory or a
   pharmacy charges for what it does without importing anything from Billing,
   because core binds a silent no-op and Billing replaces the binding when it is
   installed ([ADR 0017](adr/0017-laboratory-results-and-the-charging-port.md)).

   ```php
   $this->charges->charge(new ChargeRequest(
       patientId: $order->patient_id,
       actorId: $actor->id,
       description: $test->name,
       catalogueCode: $test->billing_code,   // the clinic's price wins
       encounterId: $order->encounter_id,
   ));
   ```

   A port must **never throw for an ordinary reason** and its default must do
   nothing quietly. Clinical work does not stop because a price is missing, and
   a warning logged on every use trains people to ignore the log.

Importable across a module boundary: `Contracts\`, `Events\`, `DTOs\`, `Enums\`.
Everything else — `Models\`, `Services\`, `Repositories\` — is internal and may
change in any release.

Events carry identifiers, never Eloquent models. A serialised model in a queued
listener is stale data waiting to overwrite something.

---

## 6. Routes are secure by default

| File | Middleware |
|---|---|
| `Routes/web.php` | `web`, `auth` |
| `Routes/public.php` | `web` only — for anything a signed-out visitor may reach |
| `Routes/api.php` | `api`, `auth:sanctum`, prefixed `api/v1`, named `api.v1.` |
| `Routes/console.php` | loaded in console context only |

Forgetting to add `auth` cannot expose patient data. Exposure has to be a
deliberate act, in a file whose name says what it does.

---

## 7. Data rules

- `HasUlid` on anything with a public identity. **Never expose an integer id** —
  URLs, API payloads and exports all use the ULID.
- `BelongsToBranch` on every operational table. `branch_id` NOT NULL. Queries are
  scoped automatically; escaping the scope requires an explicit
  `withoutBranchScope()` that is greppable and reviewable.
- `Auditable` on anything whose changes matter. Model events cover data;
  services call `AuditLogger` directly for intent ("invoice refunded",
  "record exported").
- Money is `BIGINT` minor units + a currency column, cast with `MoneyCast`.
  Never a float.
- Timestamps stored UTC.
- Document numbers come from `SequenceGenerator`, inside the transaction that
  creates the document. Never `MAX(id) + 1`.
- Soft deletes on master data. **Never** on clinical records or financial
  documents — those are amended or reversed, never edited or removed.
- Encrypted identifier columns get a companion blind-index column
  (`App\Support\Crypto\BlindIndex`) so exact-match lookup still works.

---

## 8. Migrations

Read `docs/ARCHITECTURE.md` §7.8 before writing one. In short:

- Additive within a major version. Add nullable → backfill in a chunked job →
  switch code → drop in the next major.
- Guard with `Schema::hasTable` / `hasColumn`. Interrupted updates on shared
  hosting happen, and the retry must not fail.
- Never rename or retype a column in place on a table that may hold millions of
  rows on unknown hardware.
- Data migrations are queued jobs, not migration files — a two-minute migration
  times out on shared hosting.

Remember what this means: a bad migration ships to hundreds of independent
installations you cannot SSH into. It is not a rollback, it is hundreds of
support tickets.

---

## 9. Tests

Module tests live in `Modules/<Name>/Tests`, so deleting the module directory
removes its tests with it.

Drive the **use case**, not the controller:

```php
$patient = app(PatientServiceInterface::class)->create(new PatientData(name: 'Ana'));
```

If that passes without an HTTP session, the mobile app can reach the same
behaviour. Tests run against MySQL, not SQLite — the production floor is MySQL 8
and a green SQLite suite would prove nothing about enums, fulltext indexes,
`SELECT ... FOR UPDATE` or NULL-sensitive unique indexes.

---

## 10. Before you push

```bash
composer gates
```

Runs Pint (style), PHPStan level 6, Deptrac (layer rules) and Pest (including
the architecture tests that enforce §5). All four must be green.
