# M03 — Matters

Phase 1. The central work unit. Litigation and non-litigation matters both live
here — distinguished by `matter_type`, not by separate tables — so every other
module (billing, documents, tasks) attaches to one consistent parent.

## Dependencies
M01, M02 (clients, practice_areas, matter_stages, offices, users).

## Database

### `matters`
```php
$table->id(); $table->ulid('ulid')->unique();
$table->foreignId('firm_id')->constrained()->restrictOnDelete();
$table->string('matter_number')->unique();       // MAT/2026/0001 via NumberingService
$table->string('suit_number')->nullable();       // official court suit no. (litigation only)
$table->string('title');
$table->string('matter_type');                   // Litigation / Advisory / Transactional / Corporate
$table->foreignId('client_id')->constrained()->restrictOnDelete();
$table->foreignId('practice_area_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('court_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('matter_stage_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('office_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('responsible_partner_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('client_role')->nullable();       // PartyRole enum, litigation only
$table->string('status')->default('open');       // MatterStatus enum
$table->string('priority')->default('normal');   // low/normal/high/urgent
$table->date('date_opened');
$table->date('date_closed')->nullable();
$table->date('filing_date')->nullable();
$table->text('summary')->nullable();
$table->text('relief_sought')->nullable();
$table->string('opposing_counsel')->nullable();
$table->decimal('claim_amount', 18, 2)->nullable();
$table->decimal('estimated_value', 18, 2)->nullable();  // non-litigation matter value
$table->foreignId('created_by')->constrained('users');
$table->timestamps(); $table->softDeletes();
$table->index(['firm_id', 'status']);
$table->index(['firm_id', 'client_id']);
$table->fullText(['title', 'summary']);
```

### `matter_team` (pivot)
```php
$table->foreignId('matter_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('role')->default('counsel');  // lead_counsel / counsel / paralegal
$table->date('assigned_at')->nullable();
$table->timestamps();
$table->unique(['matter_id', 'user_id']);
```

### `matter_parties`
```php
$table->id();
$table->foreignId('matter_id')->constrained()->cascadeOnDelete();
$table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete(); // link if known
$table->string('name');           // snapshot, for conflict-checking even if contact_id is null
$table->string('role');           // PartyRole enum
$table->string('phone')->nullable();
$table->string('address')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
```

### `matter_events` (timeline)
```php
$table->id();
$table->foreignId('matter_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('type');   // registered / stage_changed / status_changed / hearing_held /
                          // document_added / task_completed / invoice_raised / note_added…
$table->string('title');
$table->text('description')->nullable();
$table->dateTime('happened_at');
$table->timestamps();
$table->index(['matter_id', 'happened_at']);
```

### `matter_comments`
Mirrors `task_comments` (see M05): `matter_id, user_id, comment, timestamps`.

## Enums (`app/Enums`)

```php
MatterType:     Litigation, Advisory, Transactional, Corporate
MatterStatus:   Open, OnHold, Settled, Won, Lost, Withdrawn, Closed
MatterPriority: Low, Normal, High, Urgent
PartyRole:      Plaintiff, Defendant, Appellant, Respondent, Claimant,
                Witness, InterestedParty, ThirdParty
```

## Model: `Matter`

`protected $table = 'matters';` — no PHP reserved-word conflicts in a fresh
build (no need for a `CaseFile` workaround this time). Relations: `client`,
`practiceArea`, `court`, `stage`, `office`, `responsiblePartner`,
`team()` (belongsToMany users, withPivot role), `parties`, `events`,
`comments`, plus later phases: `tasks`, `hearings`, `deadlines`, `timeEntries`,
`invoices`, `documents` (media). Scope `visibleTo(User $user)` — implements the
row-level rule from `04-SECURITY.md`:
```php
if ($user->can('matters.viewAll')) return $query;
return $query->whereHas('team', fn ($q) => $q->where('user_id', $user->id))
             ->orWhere('responsible_partner_id', $user->id);
```
`MatterObserver` writes `matter_events` rows on create/stage-change/status-change.
`LogsActivity` (activitylog) trait for full audit.

## Matter creation flow (Action: `App\Actions\Matters\RegisterMatter`)

Transactional: validate → conflict-check surfaced (non-blocking modal, decision
logged per M02) → `NumberingService::next('matter')` → create matter → attach
team (≥1 required, first = lead_counsel by default) → attach parties → write
`matter_events` "registered" → if `matter_type = Litigation` and a task template
exists ("New Suit Filing" checklist), dispatch task generation (M05) → fire
`MatterRegistered` event (for notifications).

## Routes

RESTful `matters` resource, plus:
```
POST /matters/{matter}/team              matters.team.store
DELETE /matters/{matter}/team/{user}     matters.team.destroy
POST /matters/{matter}/parties           matters.parties.store
PATCH /matters/{matter}/stage            matters.stage.update
PATCH /matters/{matter}/status           matters.status.update
POST /matters/{matter}/comments          matters.comments.store
GET  /matters/search                     matters.search   (Select2 AJAX)
```

## Views

`matters/index`: KPI strip (open, on hold, closed this month, urgent), filters
(status, practice area, stage, responsible partner, client), table (matter no.,
title, client, court, stage, next hearing — populated once M04 lands, team
avatars, status badge). `matters/show`: tabbed detail — Overview | Parties |
Team | Timeline, with placeholder tabs for Hearings/Tasks/Billing/Documents that
later phases fill in (build the tab shell now so the UI doesn't need rework).

## Acceptance criteria

- [ ] Matter registration generates a unique number under concurrency; conflict
      check surfaces and decision logs
- [ ] Team requires ≥1 member; removing the last member is blocked
- [ ] Stage/status changes append timeline events + activity log entries
- [ ] Litigation matters expose suit number, client role, opposing counsel;
      non-litigation matters hide litigation-only fields in the UI
- [ ] `visibleTo` scope correctly restricts non-partners to their assigned matters
- [ ] Full-text search on title/summary returns relevant matters
- [ ] Cross-firm isolation + permission-denial tests green
- [ ] Detail page tab shell renders correctly for all future module placeholders

## Claude Code kickoff prompt

> Read CLAUDE.md, docs/03-DATA-MODEL.md, docs/04-SECURITY.md, docs/05-UI-UX.md,
> and this file. Implement M03: migrations → enums → Matter model +
> MatterObserver + `visibleTo` scope → RegisterMatter Action (test the
> concurrency-safe numbering and the ≥1-team-member rule first) → FormRequests →
> MatterController + team/parties/stage/status sub-controllers → views (index +
> tabbed show with placeholder tabs) → Pest tests. This module's `Matter` model
> is the anchor every later module attaches to — keep relation names and the
> `visibleTo` scope stable since M04–M10 all depend on them.
