---
description: Enforces migration-first development. Schema is source of truth.
globs: ["database/migrations/**", "app/Domain/**/Models/**", "app/Models/**"]
---

# Migration First — Schema Is Source of Truth

## Before Writing Any Model

1. The migration for this table MUST already exist
2. Model `$fillable` columns are COPIED from migration — not typed from memory
3. Model `$casts` match the migration column types exactly
4. Enum values match the CHECK constraint values character-for-character

## Migration Standards

- Table names: plural snake_case (`training_programs`, `invoice_items`)
- FK columns: `{singular_table}_id` (`academy_id`, `participant_id`)
- Booleans: `is_` or `has_` prefix, always have `->default()`
- Money: `bigInteger` (piasters, not pounds, not decimal)
- Status/type: `string(30)` with CHECK constraint via `DB::statement()`
- JSONB: `->default('{}')` or `->default('[]')` — never nullable
- Every tenant table has `$table->foreignId('academy_id')->constrained('organizations')`
- Every public entity has `$table->uuid('uuid')->unique()`
- Soft-deletable entities have `$table->softDeletes()`
- User-created entities have `$table->foreignId('created_by')->constrained('users')`

## CHECK Constraint Pattern

```php
DB::statement("ALTER TABLE {table} ADD CONSTRAINT {table}_{column}_check CHECK ({column} IN ('value1', 'value2'))");
```

## Column Name Reference (NEVER deviate)

- Tenant: `academy_id`
- Branch: `branch_id`
- Person: `person_id`
- Participant: `participant_id`
- Group: `training_group_id`
- Program: `training_program_id`
- Session: `training_session_id`
- Invoice: `invoice_id`
- Facility: `facility_id`
- Creator: `created_by`
- Approver: `approved_by`
- Product: `product_id`
- Warehouse: `warehouse_id`

## Index Rules

- `nullableMorphs()` already creates composite index — don't duplicate
- `foreignId()->constrained()` already creates single-column index
- Always index: `academy_id`, `status`, `created_at`
- Composite: leftmost column is the most selective filter
