# Matters — BAMS Legal

> The central work unit. Litigation and non-litigation matters both live here,
> distinguished by `matter_type` — every other module (billing, documents,
> hearings, tasks) attaches to a matter.

---

## Table of Contents

1. [Matter](#matter)
2. [Matter Team](#matter-team)
3. [Matter Party](#matter-party)
4. [Matter Event (Timeline)](#matter-event-timeline)
5. [Matter Comment](#matter-comment)
6. [Subscription — err, Matter Lifecycle](#matter-lifecycle)
7. [Notes & Guidelines](#notes--guidelines)

---

## Matter

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `ulid` | ULID | Auto | — | Public identifier |
| `firm_id` | FK | Yes | — | Owning firm |
| `matter_number` | String | Auto | — | `MAT/2026/0001`, via NumberingService |
| `suit_number` | String | No | `null` | Official court suit number (litigation only) |
| `title` | String | Yes | — | e.g. "A.B. Ltd v. C.D." |
| `matter_type` | Enum | Yes | — | `litigation`, `advisory`, `transactional`, `corporate` |
| `client_id` | FK | Yes | — | |
| `practice_area_id` | FK | No | `null` | |
| `court_id` | FK | No | `null` | Litigation only |
| `matter_stage_id` | FK | No | `null` | |
| `office_id` | FK | No | `null` | |
| `responsible_partner_id` | FK (User) | No | `null` | |
| `client_role` | Enum | No | `null` | `PartyRole` — litigation only |
| `status` | Enum | Yes | `open` | `open`, `on_hold`, `settled`, `won`, `lost`, `withdrawn`, `closed` |
| `priority` | Enum | No | `normal` | `low`, `normal`, `high`, `urgent` |
| `date_opened` | Date | Yes | — | |
| `date_closed` | Date | No | `null` | |
| `filing_date` | Date | No | `null` | |
| `summary` | Text | No | `null` | |
| `relief_sought` | Text | No | `null` | Litigation only |
| `opposing_counsel` | String | No | `null` | |
| `claim_amount` | Decimal(18,2) | No | `null` | |
| `estimated_value` | Decimal(18,2) | No | `null` | Non-litigation value |

### Admin Routes

**Base:** `/admin/matters` · **Auth:** session, permission `matters.view` / `matters.viewAll`

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/matters` | List (paginated; scoped to assigned-only unless `matters.viewAll`) |
| `GET` | `/admin/matters/:id` | Get matter detail (tabbed: Overview, Parties, Team, Timeline, Hearings, Tasks, Billing, Documents) |
| `POST` | `/admin/matters` | Register a new matter |
| `PATCH` | `/admin/matters/:id` | Update matter fields |
| `PATCH` | `/admin/matters/:id/stage` | Change stage |
| `PATCH` | `/admin/matters/:id/status` | Change status (close/settle/win/lose) |
| `DELETE` | `/admin/matters/:id` | Soft delete (permission `matters.delete`) |
| `GET` | `/admin/matters/search?q=` | Live search (Select2-style, used across other modules) |

### Request & Response Examples

**Register a matter** — `POST /admin/matters`

```json
{
  "title": "Zenith Traders Ltd v. Coastal Freight Ltd",
  "matter_type": "litigation",
  "client_id": 88,
  "practice_area_id": 4,
  "court_id": 2,
  "client_role": "plaintiff",
  "date_opened": "2026-07-01",
  "claim_amount": 15000000,
  "team": [{ "user_id": 12, "role": "lead_counsel" }],
  "parties": [{ "name": "Coastal Freight Ltd", "role": "defendant" }]
}
```

```json
{
  "success": true,
  "data": { "id": 210, "matter_number": "MAT/2026/0055", "status": "open" },
  "message": "Matter registered. 1 conflict-check match found — reviewed and cleared."
}
```

**Change status** — `PATCH /admin/matters/:id/status`

```json
{ "status": "won", "note": "Judgment delivered in our client's favor" }
```

```json
{ "success": true, "message": "Matter status updated to Won." }
```

### Validation Rules

| Field | Rules |
|---|---|
| `title` | Required. Max 255. |
| `matter_type` | Required. One of the four enum values. |
| `client_id` | Required. Must exist and belong to the firm. |
| `date_opened` | Required. Cannot be in the future. |
| `team` | Required, min 1 member. First entry defaults to `lead_counsel` if no role given. |
| `claim_amount` / `estimated_value` | Optional. Numeric, min 0. |
| `status` transition | `closed` is only reachable from `settled`/`won`/`lost`/`withdrawn`, never directly from `open`. |

---

## Matter Team

### Schema (pivot)

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `matter_id` | FK | Yes | — | |
| `user_id` | FK | Yes | — | |
| `role` | Enum | No | `counsel` | `lead_counsel`, `counsel`, `paralegal` |
| `assigned_at` | Date | No | `null` | |

### Admin Routes

| Method | Endpoint | Description |
|---|---|---|
| `POST` | `/admin/matters/:id/team` | Add a team member |
| `DELETE` | `/admin/matters/:id/team/:userId` | Remove a team member |

### Validation Rules

| Field | Rules |
|---|---|
| `user_id` | Required. Must be an active user. Cannot duplicate an existing member. |
| Removal | Blocked if it would leave the matter with zero team members. |

---

## Matter Party

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `matter_id` | FK | Yes | — | |
| `contact_id` | FK | No | `null` | Linked contact, if known |
| `name` | String | Yes | — | Snapshot — kept even if `contact_id` is null, for conflict-checking |
| `role` | Enum | Yes | — | `PartyRole`: plaintiff, defendant, appellant, respondent, claimant, witness, interested_party, third_party |
| `phone` / `address` | String | No | `null` | |
| `notes` | Text | No | `null` | |

### Admin Routes

| Method | Endpoint | Description |
|---|---|---|
| `POST` | `/admin/matters/:id/parties` | Add a party |
| `PATCH` | `/admin/parties/:id` | Update a party |
| `DELETE` | `/admin/parties/:id` | Remove a party |

---

## Matter Event (Timeline)

Read-only from the API's perspective — written internally by other modules
(hearing recorded, document added, task completed, invoice raised) via an
Observer, never created directly through a public write endpoint.

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `matter_id` | FK | Yes | — | |
| `user_id` | FK | No | `null` | System-generated events may have no user |
| `type` | String | Yes | — | `registered`, `stage_changed`, `status_changed`, `hearing_held`, `document_added`, `task_completed`, `invoice_raised`, `note_added`… |
| `title` | String | Yes | — | |
| `description` | Text | No | `null` | |
| `happened_at` | Timestamp | Yes | — | |

### Admin Routes

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/matters/:id/timeline` | Get the full chronological timeline |
| `POST` | `/admin/matters/:id/timeline/note` | Add a manual note-type event |

---

## Matter Comment

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `matter_id` | FK | Yes | — | |
| `user_id` | FK | Yes | — | |
| `comment` | Text | Yes | — | |

### Admin Routes

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/matters/:id/comments` | List discussion thread |
| `POST` | `/admin/matters/:id/comments` | Post a comment (notifies matter team, see M09) |

---

## Matter Lifecycle

```
POST /admin/matters
        │
        ▼
  status = "open"
  matter_number generated
  team + parties attached
  timeline event: "registered"
  (if a matching task template exists) checklist tasks auto-generated
        │
        ▼ (ongoing — stage progresses as the matter proceeds)
  PATCH /admin/matters/:id/stage
  timeline event: "stage_changed"
        │
        ▼ (matter concludes)
  PATCH /admin/matters/:id/status  → settled / won / lost / withdrawn
  timeline event: "status_changed"
        │
        ▼ (administrative closure, only from a terminal status)
  PATCH /admin/matters/:id/status  → closed
  date_closed = now()
```

---

## Notes & Guidelines

- **Assigned-only visibility:** a user without `matters.viewAll` only sees
  matters where they're on the team or are the `responsible_partner` — this
  scope is applied identically across list, search, dashboards, and reports;
  it is never re-implemented per endpoint.
- **`matter_number` is immutable** once generated — concurrent registration
  requests never produce duplicates (locked sequence, see M01 NumberingService).
- Litigation-only fields (`suit_number`, `client_role`, `relief_sought`,
  `opposing_counsel`) are hidden in the UI for non-litigation matter types but
  remain in the schema — no separate table needed.
- There is no public or portal write route for matters. The M11 Client Portal
  exposes a **read-only, sanitized** view of a client's own matters — a
  completely separate serializer, not this API.
