# M01 — Foundation & Administration

Phase 0. Everything else depends on this module. Read `docs/02-ARCHITECTURE.md`
and `docs/04-SECURITY.md` first — this module is where those rules become code.

## Scope

Laravel install & CI, firm/tenancy skeleton, authentication (incl. 2FA), RBAC,
settings, offices, reference data, numbering, audit logging, base layouts, and
the security/scalability scaffolding (FirmScope, headers, route-audit test).

## Database

### `firms`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->string('name');
$table->string('legal_name')->nullable();
$table->string('case_prefix', 10)->default('MAT');
$table->string('logo_path')->nullable();
$table->string('primary_color', 7)->nullable();
$table->string('accent_color', 7)->nullable();
$table->string('timezone')->default('Africa/Lagos');
$table->timestamps();
```

### `users`
Standard Laravel fields + `firm_id` (nullable — superadmins may be firm-less in
future SaaS mode, but v1: required), `two_factor_secret`/`two_factor_recovery_codes`
(encrypted, Fortify-style), `status` (Active/Suspended), `last_login_at`.

### `roles` / `permissions` / pivot tables
Standard Spatie tables (`role_has_permissions`, `model_has_roles`,
`model_has_permissions`), scoped by adding `firm_id` to `roles` (Spatie supports
teams mode — enable `teams` feature keyed on `firm_id`).

### `offices`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('name'); $table->string('address');
$table->string('city')->nullable(); $table->string('state')->nullable();
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->string('phone')->nullable(); $table->string('email')->nullable();
$table->boolean('is_head_office')->default(false);
$table->boolean('is_active')->default(true);
$table->timestamps();
```

### Reference tables (identical shape)
`courts` (+ `type` CourtType enum, `location`), `practice_areas`, `matter_stages`
(+ `sort_order`), `contact_categories`, `document_types`:
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('name'); $table->string('slug')->nullable();
$table->unsignedSmallInteger('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
```

### `settings`
```php
$table->id();
$table->foreignId('firm_id')->constrained()->cascadeOnDelete();
$table->string('key');
$table->json('value')->nullable();
$table->timestamps();
$table->unique(['firm_id', 'key']);
```

### `number_sequences`
```php
$table->id();
$table->foreignId('firm_id')->constrained()->cascadeOnDelete();
$table->string('key');            // matter, invoice, receipt, claim
$table->unsignedSmallInteger('year');
$table->unsignedInteger('last_number')->default(0);
$table->unique(['firm_id', 'key', 'year']);
```

## Services

- `NumberingService::next(string $key): string` — locks the sequence row
  (`lockForUpdate` inside `DB::transaction`), increments, formats
  `{prefix}/{year}/{padded 4-digit}`.
- `SettingsService` — cached (`Cache::tags("firm:{$id}")`) typed get/set, backed
  by `settings` table.
- `FirmScope` (global scope) + `BelongsToFirm` trait — auto-applies
  `where('firm_id', app(CurrentFirm::class)->id())` to every tenant model query,
  auto-fills `firm_id` on create.
- `CurrentFirm` — resolved from the authenticated user (v1: effectively a
  singleton firm; kept as a service so multi-firm later is a config change, not
  a rewrite).

## Authentication & 2FA

- Laravel's built-in auth scaffolding + `pragmarx/google2fa` (or Fortify 2FA
  actions) for TOTP; recovery codes hashed at rest.
- `EnforceTwoFactor` middleware: blocks access to `finance.*` and `admin.*`
  routes until 2FA is enabled, for any role holding a permission flagged
  `requires_2fa` in the seeder.
- Login throttling: `RateLimiter::for('login', ...)` keyed by email+IP, 5
  attempts / 60s lockout with exponential backoff on repeat.

## Permission matrix (seed exactly this list; grouped by module for readability)

```
Foundation:      firm.manage, users.manage, roles.manage, settings.manage,
                 offices.manage, reference_data.manage, audit.view, backups.manage
Contacts/Clients: contacts.view, contacts.viewAll, contacts.create, contacts.edit,
                 contacts.delete, clients.export
Matters:         matters.view, matters.viewAll, matters.create, matters.edit,
                 matters.close, matters.delete, matters.assignTeam
Litigation:      hearings.view, hearings.create, hearings.edit, hearings.delete,
                 roster.view, roster.manage, roster.confirmAttendance,
                 deadlines.view, deadlines.manage
Tasks:           tasks.view, tasks.create, tasks.edit, tasks.delete
Billing:         time.view, time.create, time.edit, billing.invoice,
                 billing.viewAll
Finance:         finance.view, finance.viewAll, finance.post, finance.approve,
                 finance.reverse, trust.manage, appearance_fees.create,
                 appearance_fees.approve, appearance_fees.pay
Documents:       documents.view, documents.viewConfidential, documents.upload,
                 documents.manageVersions, documents.manageEvidence
Communication:   announcements.manage, messages.send
Reports:         reports.view, dashboard.executive
Portal/Site:     portal.manageAccess, site.manageContent
```

Roles seeded: `Managing Partner` (all), `Partner` (all except firm.manage/
users.manage), `Counsel` (assigned-only view + create/edit within team),
`Paralegal` (litigation/tasks/documents, no finance), `Accounts` (billing/finance
full, no litigation edit), `Firm Admin` (foundation + reference data + backups,
no case-content access by default).

Permissions flagged `requires_2fa`: `finance.*`, `users.manage`, `roles.manage`,
`backups.manage`.

## Middleware & base layouts

- `EnsureFirmContext`, `EnsurePermission` (thin wrapper enforcing named
  permission + logging denials to the security log), `SecurityHeaders`
  (CSP/X-Frame-Options/etc.), `EnforceTwoFactor`.
- Three base layouts (`layouts.app`, `layouts.portal`, `layouts.site`) each
  pulling branding from `SettingsService`/`firms` table via CSS custom
  properties.

## Route-audit test (build this first, keep it green forever)

A Pest test that iterates every registered route under `web`/`portal` groups and
asserts each has either a `permission:` middleware entry or is explicitly
allow-listed (login, logout, health check). This is the backbone of
the security posture — every future module is caught by it automatically.

## Acceptance criteria

- [ ] Fresh install boots; CI (Pest/Pint/Larastan/`composer audit`) green on an
      empty-but-scaffolded app
- [ ] Login + TOTP 2FA works; lockout after 5 failed attempts; recovery codes work
- [ ] Role/permission matrix seeded exactly as above; Firm Admin can manage roles
- [ ] `NumberingService` produces unique sequential numbers under concurrent
      requests (test spawns parallel calls, asserts no duplicates)
- [ ] `FirmScope` proven via a dummy tenant model + cross-firm isolation test
- [ ] Route-audit test passes and fails correctly when a new unprotected route is added
- [ ] Security headers present on every response (test)
- [ ] Offices & all reference-data CRUD screens work end-to-end, following
      05-UI-UX.md list/form patterns
- [ ] `docs/05a-TEMPLATE-MAP.md` produced from the actual chosen template

## Claude Code kickoff prompt

> Read CLAUDE.md, docs/02-ARCHITECTURE.md, docs/03-DATA-MODEL.md,
> docs/04-SECURITY.md, docs/05-UI-UX.md and this file. Build M01 in this order:
> Laravel 12 install + CI config → firms/users/settings/number_sequences
> migrations → FirmScope + BelongsToFirm + CurrentFirm → Spatie roles/permissions
> with teams mode on firm_id, seed the full matrix above → 2FA (Fortify actions
> or pragmarx/google2fa) + EnforceTwoFactor → NumberingService (test concurrency
> first) → offices + reference data CRUD → three base layouts + security headers
> middleware → route-audit Pest test → produce docs/05a-TEMPLATE-MAP.md by
> inspecting the provided template's components. Do not proceed to M02 until
> every acceptance criterion is green.
