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>
555 lines
31 KiB
JSON
555 lines
31 KiB
JSON
{
|
|
"info": {
|
|
"name": "be-crm — Leads & Lead Verification (data door)",
|
|
"description": "All Leads (crm.lead.*) and Lead Verification (crm.leadVerification.*) data-door actions — for local testing against be-crm on :4010 (trust-headers mode).\n\nHOW TO USE:\n1. Import this file into Postman.\n2. Run 'Verification writes / 10. intake' FIRST — its test script captures the new record id into {{seedVerificationId}}, which the assign/verify chain references. The cleanest end-to-end path is: 10 intake → 11 assign → 12 reassign → 15 verify (promotes to a Lead, captured into {{promotedLeadId}}).\n3. The read folders ('Leads reads' / 'Verification reads') capture {{leadId}} / {{verificationId}} from search results when data exists.\n\nVERIFICATION STATE MACHINE (enforced server-side, §3.1): a record flows pending → assigned → in_progress, and ends at ONE terminal outcome — either verified (via 15. verify, which promotes a Lead) OR unverified (via 17. markUnverified). 'verified' is TERMINAL: markUnverified / moveToPending / updateStatus against a verified record return 409 invalid_state_transition. So run ONE terminal action per record. To exercise markUnverified or moveToPending, run 10. intake to seed a FRESH pending record (repoints {{verificationId}}) and act on it BEFORE verifying — e.g. intake → assign → moveToPending, or intake → markUnverified. Valid: markUnverified from pending/assigned/in_progress; moveToPending from assigned/in_progress.\n\nNOTE: everything is POST except GET /health. Reads hit /query, writes hit /command (action+data in the JSON body). Commands auto-generate a fresh X-Idempotency-Key ({{$guid}}). In dev, the identity headers stand in for what the Shell BFF injects in real environments — never ship them from the frontend. Both modules are gated by the leads.manage permission.",
|
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
},
|
|
"variable": [
|
|
{ "key": "baseUrl", "value": "http://localhost:4010" },
|
|
{ "key": "tenant", "value": "tnt_demo" },
|
|
{ "key": "principal", "value": "prn_owner" },
|
|
{ "key": "leadId", "value": "" },
|
|
{ "key": "verificationId", "value": "" },
|
|
{ "key": "seedVerificationId", "value": "" },
|
|
{ "key": "promotedLeadId", "value": "" },
|
|
{ "key": "createdLeadId", "value": "" },
|
|
{ "key": "attachmentId", "value": "" }
|
|
],
|
|
"item": [
|
|
{
|
|
"name": "0. Health (the only GET)",
|
|
"request": {
|
|
"method": "GET",
|
|
"header": [],
|
|
"url": { "raw": "{{baseUrl}}/health", "host": ["{{baseUrl}}"], "path": ["health"] },
|
|
"description": "Liveness + the full list of registered actions (crm.lead.* + crm.leadVerification.*). Works in a browser."
|
|
}
|
|
},
|
|
{
|
|
"name": "Leads reads (POST /query)",
|
|
"item": [
|
|
{
|
|
"name": "1. lead.stats (header stat strip)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.stats\",\n \"variables\": {}\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
|
|
"description": "Returns { total, byStatus: { new, contacted, appointed, closed } }."
|
|
}
|
|
},
|
|
{
|
|
"name": "2. lead.search (captures leadId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j.items && j.items[0]) pm.collectionVariables.set('leadId', j.items[0].id);",
|
|
"pm.test('has paging meta', () => pm.expect(j.meta).to.have.property('total'));"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.search\",\n \"variables\": { \"status\": \"new\", \"page\": 1, \"perPage\": 50 }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
|
|
"description": "Board list + search + status/priority/source/assigneeId filters. Returns { items: LeadCardDTO[], meta }."
|
|
}
|
|
},
|
|
{
|
|
"name": "3. lead.get (full detail)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j.attachments && j.attachments[0]) pm.collectionVariables.set('attachmentId', j.attachments[0].id);"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.get\",\n \"variables\": { \"id\": \"{{leadId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
|
|
"description": "Full record incl. phones/emails/attachments + resolved assignee/canvasser/creator names + storm/property/job/insurance groupings."
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Verification reads (POST /query)",
|
|
"item": [
|
|
{
|
|
"name": "4. leadVerification.stats (stat tiles)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.stats\",\n \"variables\": {}\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
|
|
"description": "Returns { verified, in_progress, assigned, pending, unverified }."
|
|
}
|
|
},
|
|
{
|
|
"name": "5. leadVerification.search (captures verificationId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j.items && j.items[0]) pm.collectionVariables.set('verificationId', j.items[0].id);",
|
|
"pm.test('has paging meta', () => pm.expect(j.meta).to.have.property('total'));"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.search\",\n \"variables\": { \"page\": 1, \"perPage\": 50 }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] },
|
|
"description": "Queue table + search + status/source/assignee filters. Returns { items: VerificationRowDTO[], meta }."
|
|
}
|
|
},
|
|
{
|
|
"name": "6. leadVerification.get (detail + timeline)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.get\",\n \"variables\": { \"id\": \"{{verificationId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "7. leadVerification.activity.list",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.activity.list\",\n \"variables\": { \"id\": \"{{verificationId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/query", "host": ["{{baseUrl}}"], "path": ["query"] }
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Verification writes (POST /command)",
|
|
"item": [
|
|
{
|
|
"name": "10. leadVerification.intake (RUN FIRST — captures seedVerificationId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j && j.id) { pm.collectionVariables.set('seedVerificationId', j.id); pm.collectionVariables.set('verificationId', j.id); }",
|
|
"pm.test('starts pending', () => pm.expect(j.status).to.eql('pending'));"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.intake\",\n \"variables\": {\n \"name\": \"Kevin Hartley\",\n \"phone\": \"(972) 413-8902\",\n \"email\": \"kevin.hartley@example.com\",\n \"address\": \"2814 Ravenswood Dr, Plano, TX 75023\",\n \"source\": \"Door Knock\"\n }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Records 'arrive' via intake (§12.11) — the ingestion path outside the two UI screens. Seeds the queue and captures {{seedVerificationId}} for the working flow below."
|
|
}
|
|
},
|
|
{
|
|
"name": "11. leadVerification.assign (pending → assigned)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.assign\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\", \"assigneeId\": \"{{principal}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Change Assignee. A pending record moves to 'assigned'; otherwise only the assignee changes."
|
|
}
|
|
},
|
|
{
|
|
"name": "12. leadVerification.reassign (→ In Progress)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.reassign\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\", \"assigneeId\": \"{{principal}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "13. leadVerification.updateStatus (generic; not 'verified')",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.updateStatus\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\", \"status\": \"in_progress\", \"subStatus\": \"Reviewing Insurance\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Generic status + sub-status set. Setting 'verified' here → 422 (use 15. verify, which promotes a Lead). Disallowed transitions → 409."
|
|
}
|
|
},
|
|
{
|
|
"name": "14. leadVerification.note.add",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.note.add\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\", \"text\": \"Ownership confirmed via county records.\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "15. leadVerification.verify (PROMOTES to Lead — captures promotedLeadId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j.lead && j.lead.id) pm.collectionVariables.set('promotedLeadId', j.lead.id);",
|
|
"pm.test('verification is verified', () => pm.expect(j.verification.status).to.eql('verified'));",
|
|
"pm.test('promoted lead is new', () => pm.expect(j.lead.status).to.eql('new'));"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.verify\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\", \"notes\": \"Verified — insurance + damage confirmed.\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "The critical cross-module command: flips the record to verified and creates a new Lead (status=new) in one transaction, linking both records. Returns { verification, lead }. A second call → 409 already_verified."
|
|
}
|
|
},
|
|
{
|
|
"name": "16. leadVerification.moveToPending",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.moveToPending\",\n \"variables\": { \"id\": \"{{verificationId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Send an assigned/in_progress record back to pending (keeps the assignee). Point at a non-verified {{verificationId}}."
|
|
}
|
|
},
|
|
{
|
|
"name": "17. leadVerification.markUnverified",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.markUnverified\",\n \"variables\": { \"id\": \"{{verificationId}}\", \"reason\": \"Could not confirm ownership.\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Terminal outcome. Allowed from pending/assigned/in_progress; a verified record → 409. Point {{verificationId}} at a NON-verified record (run 10. intake to seed a fresh pending one)."
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Leads writes (POST /command)",
|
|
"item": [
|
|
{
|
|
"name": "20. lead.create (Full Form — captures createdLeadId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j && j.id) { pm.collectionVariables.set('createdLeadId', j.id); pm.collectionVariables.set('leadId', j.id); }",
|
|
"pm.test('priority normalized to lowercase', () => pm.expect(j.priority).to.eql('high'));"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.create\",\n \"variables\": {\n \"firstName\": \"John\",\n \"lastName\": \"Martinez\",\n \"phones\": [\n { \"number\": \"(469) 500-1000\", \"type\": \"Mobile\", \"primary\": true },\n { \"number\": \"(469) 500-2000\", \"type\": \"Home\" }\n ],\n \"emails\": [ { \"address\": \"john.martinez@example.com\", \"primary\": true } ],\n \"property\": { \"address\": \"4821 Spring Creek Pkwy\", \"city\": \"Plano\", \"state\": \"TX\", \"zip\": \"75023\", \"type\": \"Single Family\" },\n \"job\": { \"source\": \"Door Knock\", \"canvasserId\": \"{{principal}}\", \"leadType\": \"Insurance\", \"workType\": \"Roof Replacement\", \"tradeType\": \"Roofing\", \"urgency\": \"High\", \"notes\": \"Significant granule loss on the south slope.\" },\n \"insurance\": { \"company\": \"State Farm\", \"claimStatus\": \"Filed\", \"claimNumber\": \"CLM-2026-1000\", \"policyNumber\": \"POL-080000\", \"adjusterName\": \"Marcus Powell\", \"adjusterPhone\": \"(972) 700-3000\" },\n \"assignment\": { \"assigneeId\": \"{{principal}}\", \"priority\": \"High\", \"followUp\": \"2026-06-04\" }\n }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Quick + Full form intake. Only a non-empty name (firstName OR lastName) is required (else 422 name_required). Title-case priority is normalized to lowercase; the first phone is marked primary when none is flagged. DUPLICATE GUARD: if an active (non-closed) lead in the tenant has BOTH the same normalized primary phone AND the same address, the create is REJECTED with 409 duplicate_lead (the response's duplicateOf lists the existing lead) and no lead is inserted."
|
|
}
|
|
},
|
|
{
|
|
"name": "21. lead.update (partial patch)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.update\",\n \"variables\": { \"id\": \"{{createdLeadId}}\", \"patch\": { \"priority\": \"Medium\", \"jobNotes\": \"Adjuster meeting scheduled.\" } }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "22. lead.updateStatus (audited)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.updateStatus\",\n \"variables\": { \"id\": \"{{createdLeadId}}\", \"status\": \"contacted\", \"note\": \"Spoke with homeowner.\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Appends a row to lead_status_history."
|
|
}
|
|
},
|
|
{
|
|
"name": "23. lead.assign (rep; null for Unassigned)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.assign\",\n \"variables\": { \"id\": \"{{createdLeadId}}\", \"assigneeId\": \"{{principal}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "24. lead.attachment.add (site photo — captures attachmentId)",
|
|
"event": [
|
|
{
|
|
"listen": "test",
|
|
"script": {
|
|
"type": "text/javascript",
|
|
"exec": [
|
|
"const j = pm.response.json();",
|
|
"if (j.attachments && j.attachments.length) pm.collectionVariables.set('attachmentId', j.attachments[j.attachments.length - 1].id);"
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.attachment.add\",\n \"variables\": { \"id\": \"{{createdLeadId}}\", \"attachment\": { \"contentRef\": \"obj/site-photo-1.jpg\", \"mimeType\": \"image/jpeg\", \"sizeBytes\": 1048576, \"filename\": \"roof-south.jpg\" } }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "contentRef is the object key returned by crm.media.presignUpload after the browser PUTs the bytes (§11.5). Files over 25 MB → 413 file_too_large."
|
|
}
|
|
},
|
|
{
|
|
"name": "25. lead.attachment.remove",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.attachment.remove\",\n \"variables\": { \"id\": \"{{createdLeadId}}\", \"attachmentId\": \"{{attachmentId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Run 24 first so {{attachmentId}} is captured."
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Negative / guard checks",
|
|
"item": [
|
|
{
|
|
"name": "Verify an already-verified record (expect 409 already_verified)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.verify\",\n \"variables\": { \"id\": \"{{seedVerificationId}}\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "Run 15. verify first — a second verify on the same record is rejected (no duplicate Lead)."
|
|
}
|
|
},
|
|
{
|
|
"name": "Create with no name (expect 422 name_required)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.create\",\n \"variables\": { \"firstName\": \"\", \"lastName\": \" \" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "Create with a bad email (expect 400 validation)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.lead.create\",\n \"variables\": { \"firstName\": \"Jane\", \"emails\": [ { \"address\": \"not-an-email\" } ] }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] }
|
|
}
|
|
},
|
|
{
|
|
"name": "Verify via generic updateStatus (expect 422 — use verify)",
|
|
"request": {
|
|
"method": "POST",
|
|
"header": [
|
|
{ "key": "Authorization", "value": "Bearer dev" },
|
|
{ "key": "X-App-ID", "value": "crm-web" },
|
|
{ "key": "X-Tenant-ID", "value": "{{tenant}}" },
|
|
{ "key": "X-Principal-ID", "value": "{{principal}}" },
|
|
{ "key": "X-Idempotency-Key", "value": "{{$guid}}" },
|
|
{ "key": "Content-Type", "value": "application/json" }
|
|
],
|
|
"body": { "mode": "raw", "raw": "{\n \"action\": \"crm.leadVerification.updateStatus\",\n \"variables\": { \"id\": \"{{verificationId}}\", \"status\": \"verified\" }\n}" },
|
|
"url": { "raw": "{{baseUrl}}/command", "host": ["{{baseUrl}}"], "path": ["command"] },
|
|
"description": "The generic status setter refuses 'verified' — verification must go through crm.leadVerification.verify (which promotes a Lead)."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|