# Leads — Backend Changes Required (be-crm)

Follow-ups the **frontend now depends on**. The frontend side of each item below is
already implemented and shipped on branch `integration-lead-and-verification-be`; these
are the matching changes needed in **be-crm** (`crm.lead.*` / `crm.media.*`) for the
features to work end-to-end.

Companion to `LEADS_BACKEND_REQUIREMENTS.md` (the full contract). Section refs (§) point there.

**How this was found:** integration testing the live data door at `http://localhost:4010`
(trust-headers dev mode: `X-Tenant-ID: tnt_demo`, `X-Principal-ID: prn_owner`).

---

## 1. Resolve assignee / creator IDs to member display names in `LeadDTO`

**Status:** IDs are stored correctly; **display names are not resolved.**

### What we observed
`crm.lead.create` with `assignment.assigneeId` → the id persists, and `crm.lead.get`
returns the assignee/creator — but the `name` is just the **raw principal id echoed back**,
and the dedicated `*Name` fields come back empty:

```jsonc
// crm.lead.get response (relevant fields)
"assignedToId":  "prn_owner",
"assignedToName": "",                                             // ← empty, not resolved
"assignee":      { "id": "prn_owner", "name": "prn_owner", "initials": "P" },  // name = the id
"createdById":   "prn_owner",
"createdByName": "",                                             // ← empty, not resolved
"createdBy":     { "id": "prn_owner", "name": "prn_owner", "initials": "P" }   // name = the id
```

The contract (§6.2) says the detail projection must carry **"resolved assignee / canvasser /
creator display names."** Today they are unresolved, so the detail popup's **Assigned To** and
**Created By** show a raw id (or nothing) instead of a person's name.

### Required change
In the `crm.lead.get` (and any `LeadDTO`-returning command) projection, **join the member
FKs to the `members` table** and populate the real `displayName`:

- `assigned_to_id` → `assignedToName` **and** `assignee.name` = member `displayName`
- `created_by_id`  → `createdByName`  **and** `createdBy.name` = member `displayName`
- `canvasser_id`   → `canvasserName`  (same treatment, §5 note 5)

Fall back to the id only when the member can't be resolved. Note the dev tenant `tnt_demo`
returns **0 members** from `crm.team.member.search` — seed members there so assignment can be
exercised (§12 note 5 / "[Seed/link required]").

### Frontend interim (already shipped — remove once backend resolves names)
`src/lib/leads-api.ts` now resolves the returned id against the local reps list
(`crm.team.member.search`, principalId → name) as a safety net, and reads the `createdBy`
**object** (previously the reader only looked at the never-populated `createdByName`, so
"Created By" rendered blank). Backend resolution is still the correct long-term source of truth.

---

## 2. Site photos — NO backend change (uses existing `crm.lead.attachment.add`)

**Status:** No new backend work. The endpoints already exist; the frontend was wired to them.

### Decision: photos persist via `crm.lead.attachment.add` AFTER create (NOT in `crm.lead.create`)
Photos are **not** sent in the create payload. The frontend creates the lead first, then calls the
existing `crm.lead.attachment.add` command once per photo, keyed by the new lead's id (postman **#24**).

### Frontend flow (already shipped)
1. User picks images in the full form → Property step.
2. Each file is uploaded to storage via the shared Media module:
   `crm.media.presignUpload { mime, sizeBytes }` → `{ objectKey, uploadUrl }`, then browser `PUT`s the bytes (§11.5).
3. On submit: `crm.lead.create` (WITHOUT photos) → then per photo:
   ```jsonc
   { "action": "crm.lead.attachment.add",
     "variables": { "id": "<newLeadId>",
       "attachment": { "contentRef": "<objectKey>", "mimeType": "image/jpeg", "sizeBytes": 12345, "filename": "roof.jpg" } } }
   ```
4. Detail popup reads `attachments[]` from `crm.lead.get` and renders each via `crm.media.presignDownload { contentRef }`.

### Backend expectations (should already hold — just confirm)
- `crm.lead.attachment.add` inserts a `lead_attachments` row and enforces the 25 MB cap → `413 file_too_large` (§4.4).
- `crm.lead.get` returns the stored photos as **`attachments: Attachment[]`**, each with at least
  `{ id, contentRef, mimeType, sizeBytes, filename }` (postman #3 / #24 already reference `j.attachments`).
- `crm.media.presignUpload` / `crm.media.presignDownload` are registered for this app/tenant (shared Media module).

### Not verified live
Could **not** confirm against `:4010` (dev backend intermittently down during testing). When stable, run:
`crm.media.presignUpload` → `crm.lead.create` → `crm.lead.attachment.add` → `crm.lead.get` and confirm
`attachments[]` comes back.

---

## Quick verification checklist (run against `:4010` when backend is up)

- [ ] Seed members in `tnt_demo`; `crm.team.member.search` returns > 0 items.
- [ ] `crm.lead.create` with `assignment.assigneeId = <member principalId>` → `crm.lead.get`
      returns `assignedToName` = that member's real display name.
- [ ] `crm.lead.get` returns `createdByName` = the creating caller's display name.
- [ ] `crm.media.presignUpload` returns `{ objectKey, uploadUrl }`; PUT to `uploadUrl` succeeds.
- [ ] `crm.lead.attachment.add { id, attachment }` → `crm.lead.get` returns matching `attachments[]`.
- [ ] `crm.media.presignDownload { contentRef }` returns a working signed URL for a stored photo.
