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

5.4 KiB

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:

// 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_idassignedToName and assignee.name = member displayName
  • created_by_idcreatedByName and createdBy.name = member displayName
  • canvasser_idcanvasserName (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 PUTs the bytes (§11.5).
  3. On submit: crm.lead.create (WITHOUT photos) → then per photo:
    { "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.presignUploadcrm.lead.createcrm.lead.attachment.addcrm.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.