# Artisan-Migrate Design

## Data Model

This unit owns no custom data model. Its persistent state lives entirely in two pre-existing structures maintained by the Laravel migrator:

| Structure | Location | Role |
|-----------|----------|------|
| `migrations` ledger table | database (framework-managed) | Records which migration classes have already been applied; the migrator reads this before each run and inserts a row per newly-applied file. |
| Migration class files | `database/migrations/` | The ordered set of DDL/DML operations the migrator executes; their run order is determined by filename timestamp prefix. |

The unit reads and writes both structures indirectly via `Artisan::call` — it holds no local state between requests.

## Internal Flows

### Happy path

```mermaid
sequenceDiagram
    participant Browser
    participant AdminMiddleware as Admin middleware<br/>['web','admin']
    participant Closure as GET /artisan<br/>closure
    participant Migrator as Artisan::call('migrate')
    participant DB as Database<br/>(migrations table + schema)

    Browser->>AdminMiddleware: GET /artisan (authenticated admin session)
    AdminMiddleware->>Closure: passes through
    Closure->>Migrator: Artisan::call('migrate', ['--force' => true])
    Migrator->>DB: reads migrations ledger → runs pending files → inserts rows
    DB-->>Migrator: DDL applied
    Migrator-->>Closure: returns int $exitCode (captured, then discarded)
    Closure-->>Browser: HTTP 200, text/html, body: "Migrating completed<br>"
```

### Alternative flows

| Scenario | Branch point | Outcome |
|----------|-------------|---------|
| Unauthenticated request | Admin middleware | `302` redirect to `auth/login`; closure never executes; no migration runs. |
| No pending migrations | `Artisan::call` | Migrator skips all files (all recorded); returns `0`; response is still `"Migrating completed<br>"`. |
| Migration throws (SQL error, bad class) | Inside `Artisan::call` | Exception propagates uncaught through the closure → framework default `500`; schema may be left partially migrated (no transaction wrapping this route). |
| Migrator returns non-zero exit without throwing | `$exitCode` assignment | Exit code is assigned to `$exitCode` then silently discarded (`//dd($exitCode);` commented out); response is still the static success string — failure is masked. |

## Technical Decisions

| Decision | Rationale / evidence | Confidence |
|----------|----------------------|------------|
| Inline closure, no controller | Route is a single-purpose maintenance trigger with no logic to isolate; the tradeoff is that it cannot be serialized by `route:cache` and is not unit-testable in isolation. | 🟢 (`routes/web.php:31-37`) |
| `--force => true` passed to `migrate` | A web-triggered call has no TTY; without `--force`, Laravel aborts with a production confirmation prompt. | 🟢 (`routes/web.php:33`) |
| Static response message, exit code discarded | The `dd($exitCode)` debug call is commented out; success and masked-failure both return `"Migrating completed<br>"`. Reviewed with the project owner 2026-09-21 — accepted as-is. | 🟢 (`routes/web.php:32,35,36`) |
| `GET` verb for a schema-mutating operation | Legacy behaviour; no CSRF barrier is required or present (GET is exempt). A safe verb means prefetch/crawler tools can trigger a production migration accidentally. Reviewed and accepted 2026-09-21. | 🟢 (`routes/web.php:31`) |
| Authentication only, no authorization/role gate | Any authenticated admin can trigger migrations. Documented in ADR-0009. | 🟡 (`config/admin.php:29`) |
| No transaction wrapper on the route | The migrator's own per-migration transactional behaviour depends on the DB engine and migration content; the route adds no outer transaction, so a failure mid-set can leave the schema partially migrated. | 🟡 (`routes/web.php:32-36`) |
| No observability | No log, metric, or audit entry is written; no record of who triggered the migration, when, or with what result. Reviewed and accepted 2026-09-21. | 🔴 (`routes/web.php:32,35` — absence) |

## Notes

- **`route:cache` incompatibility.** Closure routes are not serializable; running `php artisan route:cache` will fail if this route is present. Any migration to a controller would remove this constraint.
- **Partial-migration risk.** Because no transaction wraps the `Artisan::call`, a multi-file migration run that fails partway through leaves the schema in an intermediate state. Recovery requires manual inspection of the `migrations` table.
- **Masked non-zero exit.** The integer returned by `Artisan::call` is captured into `$exitCode` and immediately abandoned. A caller has no way to distinguish a clean run from one where the migrator reported an error without throwing — both return HTTP `200` with the same body.
- **Admin route group prefix is empty.** The path resolves to `/artisan` (not `/admin/artisan`) because `config('admin.route.prefix')` is `''`. (`config/admin.php:25`)
