# 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": "", "attachment": { "contentRef": "", "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 = ` → `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.