Files
lynkeduppro-crm/LEADS_BACKEND_CHANGES.md
T
Mayur Shinde fd2e02086e feat(leads+verification): wire Leads & Lead Verification to be-crm data door
Integrates the Leads and Lead Verification screens with the live be-crm
backend (crm.lead.* / crm.leadVerification.* / crm.media.*) behind a
mode-agnostic data layer that still falls back to the local mock when the
Shell isn't configured.

Data layer (new)
- src/lib/leads-api.ts    — crm.lead.search/get/stats/create/update/
  updateStatus/assign/sendForVerification + media (presign upload/download)
- src/lib/verify-api.ts   — crm.leadVerification.search/get/stats/verify/
  markUnverified/assign/reassign/moveToPending

Backend → FE wiring
- Assignee / creator names: read the resolved DTO fields and, as a safety
  net, resolve member ids → real names against crm.team.member.search
  (be-crm currently echoes the raw principalId as the name).
- Created By: read the createdBy object the backend returns (was blank).
- Site photos: upload via crm.media.presignUpload → PUT, persist through
  crm.lead.attachment.add (per photo, after create), render in the detail
  popup via crm.media.presignDownload.
- Duplicate guard: surface the backend's real error (detail / duplicate_lead)
  instead of the SDK's opaque "data command failed: 400".

UX
- Leads detail: Property Photos gallery + edit-mode photo add/remove.
- Verification: "Change Assignee" now opens a searchable, scrollable popup
  instead of a long inline dropdown.
- Removed the static "Storm Zone" tag from lead cards/detail; storm banner
  only renders when the lead carries storm data.

Reference / follow-ups
- leads.http, leads.postman_collection.json — be-crm data-door requests.
- LEADS_BACKEND_CHANGES.md — required be-crm changes (name resolution,
  assignee carry-over on sendForVerification) verified against :4010.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 15:20:29 +05:30

100 lines
5.4 KiB
Markdown

# 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.