Files
lynkeduppro-crm/leads.http
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

341 lines
12 KiB
HTTP

###############################################################################
# Leads & Lead Verification data-door requests (VS Code "REST Client" / JetBrains HTTP).
# Install the REST Client extension, then click "Send Request" above each block.
# Requests are chained: run the "@name" reads/writes in order so the id variables
# below resolve. The cleanest end-to-end path is:
# #10 intake #11 assign #12 reassign #15 verify (promotes to a Lead).
# Dev trust-headers mode change @principal to test different callers.
###############################################################################
@baseUrl = http://localhost:4010
@tenant = tnt_demo
@principal = prn_owner
# Shared headers are repeated per request (the format has no header includes).
### Health see every registered action (crm.lead.* + crm.leadVerification.*)
GET {{baseUrl}}/health
# LEADS: READS (POST /query)
### 1. Lead stats (header stat strip: total + byStatus)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.lead.stats", "variables": {} }
### 2. Lead search (RUN THIS provides @leadId). Supports query/status/priority/source/assigneeId/page/perPage.
# @name leadSearch
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.lead.search", "variables": { "status": "new", "page": 1, "perPage": 50 } }
@leadId = {{leadSearch.response.body.$.items[0].id}}
### 3. Lead get (full detail incl. phones/emails/attachments + resolved names)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.lead.get", "variables": { "id": "{{leadId}}" } }
# LEAD VERIFICATION: READS (POST /query)
### 4. Verification stats (the clickable stat tiles)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.leadVerification.stats", "variables": {} }
### 5. Verification search (RUN THIS provides @verificationId)
# @name vSearch
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.leadVerification.search", "variables": { "page": 1, "perPage": 50 } }
@verificationId = {{vSearch.response.body.$.items[0].id}}
### 6. Verification get (detail + activity timeline)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.leadVerification.get", "variables": { "id": "{{verificationId}}" } }
### 7. Verification activity list (also embedded in .get)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.leadVerification.activity.list", "variables": { "id": "{{verificationId}}" } }
# VERIFICATION: WRITES (POST /command + unique key)
# Commands require a UNIQUE X-Idempotency-Key each time (reuse 409).
# Records "arrive" via intake (§12.11) start the working flow at #10.
### 10. Intake seed a verification record (RUN provides @seedVerificationId)
# @name intake
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-intake-001
Content-Type: application/json
{ "action": "crm.leadVerification.intake", "variables": {
"name": "Kevin Hartley",
"phone": "(972) 413-8902",
"email": "kevin.hartley@example.com",
"address": "2814 Ravenswood Dr, Plano, TX 75023",
"source": "Door Knock"
} }
@seedVerificationId = {{intake.response.body.$.id}}
### 11. Assign (Change Assignee) pending assigned
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-assign-001
Content-Type: application/json
{ "action": "crm.leadVerification.assign", "variables": { "id": "{{seedVerificationId}}", "assigneeId": "{{principal}}" } }
### 12. Reassign In Progress
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-reassign-001
Content-Type: application/json
{ "action": "crm.leadVerification.reassign", "variables": { "id": "{{seedVerificationId}}", "assigneeId": "{{principal}}" } }
### 13. Update status + sub-status (generic; cannot set 'verified' here use #15)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-updatestatus-001
Content-Type: application/json
{ "action": "crm.leadVerification.updateStatus", "variables": { "id": "{{seedVerificationId}}", "status": "in_progress", "subStatus": "Reviewing Insurance" } }
### 14. Add a verification note (appends + timeline entry)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-note-001
Content-Type: application/json
{ "action": "crm.leadVerification.note.add", "variables": { "id": "{{seedVerificationId}}", "text": "Ownership confirmed via county records." } }
### 15. Verify PROMOTES to a new Lead (status = new). Returns { verification, lead }.
# @name verify
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-verify-001
Content-Type: application/json
{ "action": "crm.leadVerification.verify", "variables": { "id": "{{seedVerificationId}}", "notes": "Verified insurance + damage confirmed." } }
@promotedLeadId = {{verify.response.body.$.lead.id}}
### 16. Get the promoted Lead (created by #15)
POST {{baseUrl}}/query
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
Content-Type: application/json
{ "action": "crm.lead.get", "variables": { "id": "{{promotedLeadId}}" } }
### 17. Move to Pending (send an assigned/in_progress record back). Seed a second record first if needed.
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-topending-001
Content-Type: application/json
{ "action": "crm.leadVerification.moveToPending", "variables": { "id": "{{verificationId}}" } }
### 18. Mark Unverified (any non-terminal record)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lv-unverified-001
Content-Type: application/json
{ "action": "crm.leadVerification.markUnverified", "variables": { "id": "{{verificationId}}", "reason": "Could not confirm ownership." } }
# LEADS: WRITES (POST /command + unique key)
### 20. Create Lead Full Form payload (RUN provides @createdLeadId)
# @name createLead
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-create-001
Content-Type: application/json
{ "action": "crm.lead.create", "variables": {
"firstName": "John",
"lastName": "Martinez",
"phones": [
{ "number": "(469) 500-1000", "type": "Mobile", "primary": true },
{ "number": "(469) 500-2000", "type": "Home" }
],
"emails": [ { "address": "john.martinez@example.com", "primary": true } ],
"property": { "address": "4821 Spring Creek Pkwy", "city": "Plano", "state": "TX", "zip": "75023", "type": "Single Family" },
"job": { "source": "Door Knock", "canvasserId": "{{principal}}", "leadType": "Insurance", "workType": "Roof Replacement", "tradeType": "Roofing", "urgency": "High", "notes": "Significant granule loss on the south slope." },
"insurance": { "company": "State Farm", "claimStatus": "Filed", "claimNumber": "CLM-2026-1000", "policyNumber": "POL-080000", "adjusterName": "Marcus Powell", "adjusterPhone": "(972) 700-3000" },
"assignment": { "assigneeId": "{{principal}}", "priority": "High", "followUp": "2026-06-04" }
} }
@createdLeadId = {{createLead.response.body.$.id}}
### 21. Update Lead (partial patch; priority title-case is normalized to lowercase)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-update-001
Content-Type: application/json
{ "action": "crm.lead.update", "variables": { "id": "{{createdLeadId}}", "patch": { "priority": "Medium", "jobNotes": "Adjuster meeting scheduled." } } }
### 22. Update Status (audited lead_status_history)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-status-001
Content-Type: application/json
{ "action": "crm.lead.updateStatus", "variables": { "id": "{{createdLeadId}}", "status": "contacted", "note": "Spoke with homeowner." } }
### 23. Assign a rep (or null for Unassigned)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-assign-001
Content-Type: application/json
{ "action": "crm.lead.assign", "variables": { "id": "{{createdLeadId}}", "assigneeId": "{{principal}}" } }
### 24. Attachment add (site photo) contentRef comes from crm.media.presignUpload (§11.5)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-attach-add-001
Content-Type: application/json
{ "action": "crm.lead.attachment.add", "variables": { "id": "{{createdLeadId}}", "attachment": { "contentRef": "obj/site-photo-1.jpg", "mimeType": "image/jpeg", "sizeBytes": 1048576, "filename": "roof-south.jpg" } } }
### 25. Attachment remove (attachmentId from the lead's attachments[] in #3/#16)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: lead-attach-remove-001
Content-Type: application/json
{ "action": "crm.lead.attachment.remove", "variables": { "id": "{{createdLeadId}}", "attachmentId": "REPLACE_WITH_ATTACHMENT_ID" } }
# NEGATIVE / GUARD CHECKS
### Verify an already-verified record (expect 409 already_verified)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: guard-alreadyverified-001
Content-Type: application/json
{ "action": "crm.leadVerification.verify", "variables": { "id": "{{seedVerificationId}}" } }
### Create with no name (expect 422 name_required)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: guard-noname-001
Content-Type: application/json
{ "action": "crm.lead.create", "variables": { "firstName": "", "lastName": " " } }
### Create with a bad email (expect 400 validation)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: guard-bademail-001
Content-Type: application/json
{ "action": "crm.lead.create", "variables": { "firstName": "Jane", "emails": [ { "address": "not-an-email" } ] } }
### Invalid state transition verified is terminal via generic updateStatus (expect 409 / 422)
POST {{baseUrl}}/command
Authorization: Bearer dev
X-App-ID: crm-web
X-Tenant-ID: {{tenant}}
X-Principal-ID: {{principal}}
X-Idempotency-Key: guard-transition-001
Content-Type: application/json
{ "action": "crm.leadVerification.updateStatus", "variables": { "id": "{{seedVerificationId}}", "status": "pending" } }