fd2e02086e
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>
341 lines
12 KiB
HTTP
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" } }
|