Platform Leads Troubleshooting Runbook
Use this runbook when a landing-page demo/contact submission is missing from Chatwoot, the FIXX Leads screen is empty or forbidden, or the Chatwoot deep-link is missing on a lead that exists in the database.
Architecture and API shapes: Lead Module. Who may open the menu: Platform Billing — Access Control.
Scope
| Layer | Codepaths |
|---|---|
| Domain | apps/backend/src/lead/domain/lead.entity.ts, lead-search.util.ts, chatwoot-conversation-url.util.ts |
| Application | create-lead.service.ts, lead-query.service.ts |
| Infrastructure | lead.repository.ts, chatwoot-lead-destination.adapter.ts, sendgrid-lead-notification.adapter.ts |
| Interfaces | lead.controller.ts (POST /leads); platform-leads.controller.ts (GET /platform-billing/leads*) |
| PWA | PlatformBillingLeadsPage.tsx, PlatformBillingLeadDetailPage.tsx, platformBillingService.ts |
1. Symptom triage
| Symptom | Likely layer |
|---|---|
Landing form returns 400 | CreateLeadDto validation (source / locale enums, required fields) |
Landing form returns 201 but no row | Wrong API host, or looking at a different database than the landing page |
| Row exists, Chatwoot conversation does not | Adapter skipped (wrong/missing deploy names) or Chatwoot HTTP failed |
| Chatwoot conversation exists, inbox has no “Open in Chatwoot” | setChatwootRefs failed, or CHATWOOT_BASE_URL / CHATWOOT_ACCOUNT_ID missing on the API that serves the inbox |
| FIXX Leads menu missing | Remote Config pwaMenu not published, or current business is not FIXX |
List/detail 403 FlowPOS staff access only. | PlatformAdminGuard — no active FIXX super/owner business_user |
| List empty with filters | search / source / locale; % and _ are literal, not wildcards |
| Inquiry text missing on the table | Expected — inquiryMessage and notes are detail-only |
2. Capture path (POST /leads)
Expected sequence:
- Landing page
POST /leads(no auth). LeadRepository.insertwrites the row (businessIdis alwaysnullfrom the controller).- Chatwoot dispatch and SendGrid notify run in parallel.
- On Chatwoot success,
setChatwootRefsstores conversation/contact ids. - Response is
{ "id": "<uuid>" }regardless of Chatwoot/email outcome.
Checks:
- Confirm
sourceisdemoorcontactandlocaleisesoren. - A
201with an id means persist succeeded. Do not treat a missing Chatwoot ticket as a failed HTTP call. - Search the
leadtable by email/createdAt, not by Chatwoot.
3. Chatwoot dispatch skipped or failed
The adapter returns null (skip) when any of these is unset: CHATWOOT_BASE_URL, CHATWOOT_API_TOKEN, CHATWOOT_ACCOUNT_ID, and an inbox id.
Inbox id is CHATWOOT_MARKETING_INBOX_ID or, if that is empty, CHATWOOT_INBOX_ID. Production currently uses the single-inbox fallback.
Do not configure CHATWOOT_API_URL. That name is not read. An earlier version of this adapter did, while deploy set CHATWOOT_BASE_URL — every lead persisted and none reached Chatwoot. Logs: Chatwoot env vars not configured — skipping lead dispatch.
When env is present but HTTP fails:
- Contact search/create uses the prospect email.
- Conversation create uses
inbox_id: Number(inboxId)andadditional_attributes.lead_id. - Label
marketing-leadand the private activity note are best-effort; a label failure still returns conversation/contact ids if the conversation was created. - Errors log at
ERRORand still leave the201intact.
A conversation in Chatwoot without stored ids on lead means dispatch worked on Chatwoot’s side but setChatwootRefs failed (Lead <id> saved but Chatwoot ids were not persisted). The inbox cannot reconstruct the URL without the conversation id.
4. Operator inbox (GET /platform-billing/leads)
Expected UI flow:
- Select FIXX in the business switcher (
is_platform_operator). - Open Platform Billing → Leads (
/platform-billing/leads). - Search commits on submit; source/locale apply immediately.
- Row click →
/platform-billing/leads/:leadId.
Auth failures:
403from the API →platformBilling.forbiddenin the PWA. Confirm an active FIXXbusiness_userwithunique_role_namein{ super, owner }. JWT “current business” is not used.- Menu missing after a
pwaMenu.jsonedit → publish Firebase Remote Config. Temporary icon fallback:menu-icon.tspath/platform-billing/leads.
Search surprises:
- Pattern is
%term%after escaping%,_, and\. Typing%does not mean “match anything”. searchlonger than 100 characters is rejected by the query DTO (400).sizemax is 100; default 20. Sort iscreatedAtdescending only.
Chatwoot link on detail is null when:
chatwootConversationIdis null (skip, failed dispatch, or pre-ref historical row), orCHATWOOT_BASE_URL/CHATWOOT_ACCOUNT_IDare missing on the API process serving the inbox (they are read again inLeadQueryService.getLead, not copied from the row).
URL shape (derived, not stored):
{CHATWOOT_BASE_URL}/app/accounts/{CHATWOOT_ACCOUNT_ID}/conversations/{chatwootConversationId}
5. Email did not arrive
SendGrid is independent of Chatwoot. Production/beta send to LEAD_NOTIFICATION_EMAIL. Staging/localhost skip unless COMMUNICATION_EMAIL_OVERRIDE is a single valid address.
A missing email with a 201 is not a capture failure. Check DEPLOY_ENV and whether the SendGrid adapter logged a missing variable or a SendGrid error (lead.id only in that log).