# Contacts & Clients — BAMS Legal

> Unified contact model powering clients, opposing parties, and witnesses, plus
> the conflict-of-interest engine run before every new matter and client intake.

---

## Table of Contents

1. [Contact](#contact)
2. [Client](#client)
3. [Conflict Check](#conflict-check)
4. [Notes & Guidelines](#notes--guidelines)

---

## Contact

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `ulid` | ULID | Auto | — | Public identifier |
| `firm_id` | FK | Yes | — | Owning firm |
| `kind` | Enum | Yes | — | `individual`, `organization` |
| `name` | String | Yes | — | Full name or org name |
| `email` | String | No | `null` | |
| `phone` | String | No | `null` | |
| `address` | String | No | `null` | |
| `city` / `state` | String | No | `null` | |
| `contact_category_id` | FK | No | `null` | |
| `nin_encrypted` | String (encrypted) | No | `null` | NIN — **admin only, masked in UI by default** |
| `rc_number` | String | No | `null` | CAC RC number (organizations) |
| `notes` | Text | No | `null` | |

### Admin Routes

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

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/contacts` | List (paginated, full-text search on `name`) |
| `GET` | `/admin/contacts/:id` | Get a single contact, with "appears in" cross-references |
| `POST` | `/admin/contacts` | Create |
| `PATCH` | `/admin/contacts/:id` | Update |
| `DELETE` | `/admin/contacts/:id` | Soft delete |

### Validation Rules

| Field | Rules |
|---|---|
| `kind` | Required. `individual` or `organization`. |
| `name` | Required. Max 255. |
| `email` | Optional. Valid format if present. |
| `nin_encrypted` | Optional. 11 digits if present, encrypted before storage. |

### Notes & Guidelines

- `nin_encrypted` is stored via an encrypted cast and **masked by default in
  the UI** (`***-***-1234`) — revealing it is a separate, audit-logged action.
- A `Contact` can simultaneously be referenced as a client's linked individual,
  an opposing party on one matter, and a witness on another — the "appears in"
  panel on the show page surfaces all of these so staff see the full picture
  before running a conflict check.

---

## Client

### Schema

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `ulid` | ULID | Auto | — | Public identifier |
| `firm_id` | FK | Yes | — | Owning firm |
| `contact_id` | FK | Yes | — | Linked contact record |
| `client_number` | String | Auto | — | `CLI/2026/0001`, via NumberingService |
| `status` | Enum | Yes | `active` | `active`, `inactive` |
| `relationship_partner_id` | FK (User) | No | `null` | Assigned partner |
| `kyc_completed` | Boolean | No | `false` | |
| `kyc_checklist` | JSON | No | `{}` | Checkbox state: ID, CAC docs, proof of address, mandate letter |

### Admin Routes

**Base:** `/admin/clients` · **Auth:** session, permission `clients.export` for export

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/clients` | List (filter: status, relationship partner) |
| `GET` | `/admin/clients/:id` | Get client detail (tabs: Overview, Matters, Invoices, Documents, KYC) |
| `POST` | `/admin/clients` | Create (can create a new contact inline) |
| `PATCH` | `/admin/clients/:id` | Update |
| `POST` | `/admin/clients/:id/kyc` | Update KYC checklist / upload KYC documents |
| `GET` | `/admin/clients/:id/export` | Export all held data as PDF (NDPR data-subject export) |
| `POST` | `/admin/clients/:id/portal-access` | Grant/revoke client portal access (see M11) |

### Request & Response Examples

**Create a client** — `POST /admin/clients`

```json
{
  "contact": { "kind": "organization", "name": "Zenith Traders Ltd", "email": "info@zenith.ng" },
  "relationship_partner_id": 3
}
```

```json
{
  "success": true,
  "data": { "id": 88, "client_number": "CLI/2026/0042", "status": "active" },
  "message": "Client created."
}
```

**NDPR data export** — `GET /admin/clients/:id/export`

```
Content-Type: application/pdf
Content-Disposition: attachment; filename="zenith-traders-data-export.pdf"
```

### Validation Rules

| Field | Rules |
|---|---|
| `contact_id` | Required (or a valid inline `contact` payload). |
| `relationship_partner_id` | Optional. Must be a user with an active status. |
| `kyc_checklist` | JSON object; only known checklist keys accepted. |

### Notes & Guidelines

- `client_number` is generated once, immutably, via `NumberingService` —
  never editable after creation.
- The NDPR export is a **whole-record export** (contact info, matters summary,
  invoices, KYC) intended to satisfy a data-subject access request — it is
  not the same as the sanitized client-portal view (M11), which is much narrower.
- Granting portal access here is the only way a `PortalUser` gets created (M11)
  — there is no self-registration path.

---

## Conflict Check

Not a persisted "record" in the traditional sense for the check itself — the
search is live; only the **decision** is stored.

### Schema (`conflict_checks` — the audit record)

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `id` | Integer | Auto | — | Primary key |
| `firm_id` | FK | Yes | — | Owning firm |
| `searched_name` | String | Yes | — | The name that was checked |
| `matter_id` | FK | No | `null` | Set if run during matter intake |
| `result_count` | Integer | Yes | — | Number of matches found |
| `decision` | Enum | Yes | — | `proceed`, `declined`, `escalated` |
| `decided_by` | FK (User) | Yes | — | Who made the call |
| `decided_at` | Timestamp | Yes | — | |

### Admin Routes

**Base:** `/admin/conflict-check` · **Auth:** session, permission `contacts.view`

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/admin/conflict-check?name=` | Live fuzzy search across contacts, matter parties, opposing counsel |
| `POST` | `/admin/conflict-check/decide` | Record the decision after reviewing matches |

### Request & Response Examples

**Search** — `GET /admin/conflict-check?name=Zenith`

```json
{
  "success": true,
  "data": {
    "matches": [
      { "type": "client", "name": "Zenith Traders Ltd", "linked_id": 88 },
      { "type": "opposing_party", "name": "Zenith Holdings", "matter": "MAT/2025/0091" }
    ]
  }
}
```

**Record decision** — `POST /admin/conflict-check/decide`

```json
{ "searched_name": "Zenith", "matter_id": null, "result_count": 2, "decision": "escalated" }
```

```json
{ "success": true, "message": "Decision logged." }
```

### Notes & Guidelines

- **The check never auto-blocks anything** — it is an informed-decision tool.
  The system surfaces matches; a human always decides `proceed` / `declined` /
  `escalated`.
- Every search-and-decision pair is logged permanently (`conflict_checks`),
  regardless of the decision — this is the audit trail a bar association or
  professional-indemnity insurer would ask for.
- Search covers three sources simultaneously: `contacts.name`,
  `matter_parties.name` (snapshot field, so it still matches even if the party
  was never linked to a `Contact` record), and `matters.opposing_counsel`.
