# WhatsApp Verification and Manual Send Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Send manual messages to the exact visible destination and use persisted WhatsApp evidence, rather than phone syntax, for verified labels and automatic outreach. **Architecture:** Introduce a provider-independent Brazilian phone identity utility and a sparse SQLite verification cache. Keep Baileys responsible for bounded provider operations, routes responsible for persisting outcomes, and lead queries responsible for deriving the latest equivalent-number status. **Tech Stack:** Node.js 24, ESM, `node:test`, SQLite `DatabaseSync`, Baileys 7.0.0-rc14, browser JavaScript. ## Global Constraints - Work only in `MAQUINA COMERCIAL PESSOAL`. - Verify numbers on demand; never scan the full RFB base. - The visible manual number is the authoritative destination. - `telefone_valido` means syntactic phone validity only. - Automatic outreach uses only persisted `confirmado` numbers. - Send timeout means unknown outcome and must not invite retry. - Do not create a Git commit unless the user explicitly requests one. --- ### Task 1: Brazilian Phone Identity and Opt-Out Equivalence **Files:** - Create: `src/phone.mjs` - Modify: `src/scoring.mjs` - Modify: `src/whatsapp/manager.mjs` - Modify: `src/db.mjs` - Test: `tests/scoring.test.mjs` - Test: `tests/personal-backend.test.mjs` **Interfaces:** - Produces: `normalizePhone(value): string` - Produces: `equivalentPhoneNumbers(value): string[]`, exact digits first - Consumed by: validation, Baileys lookup, verification cache, and opt-out helpers - [ ] **Step 1: Write failing utility and opt-out tests** ```js assert.deepEqual(equivalentPhoneNumbers('5575999091780'), ['5575999091780', '557599091780']); assert.deepEqual(equivalentPhoneNumbers('557599091780'), ['557599091780', '5575999091780']); addOptOut(db, '5575999091780', 'solicitado'); assert.equal(isOptedOut(db, '557599091780'), true); assert.equal(removeOptOut(db, '557599091780'), true); assert.equal(isOptedOut(db, '5575999091780'), false); ``` - [ ] **Step 2: Run the focused tests and verify RED** Run: `node --test tests/scoring.test.mjs tests/personal-backend.test.mjs` Expected: FAIL because `equivalentPhoneNumbers` is absent and alternate opt-out variants are not blocked. - [ ] **Step 3: Implement the shared utility and use it in opt-outs** ```js export function normalizePhone(value = '') { return String(value).replace(/\D/g, ''); } export function equivalentPhoneNumbers(value = '') { const digits = normalizePhone(value); const numbers = digits ? [digits] : []; if (!digits.startsWith('55')) return numbers; const prefix = digits.slice(0, 4); const local = digits.slice(4); if (local.length === 9 && local.startsWith('9')) numbers.push(`${prefix}${local.slice(1)}`); if (local.length === 8 && /^[6-9]/.test(local)) numbers.push(`${prefix}9${local}`); return [...new Set(numbers)]; } ``` Replace the manager's private candidate builder and make `isOptedOut`, `addOptOut`, and `removeOptOut` query or delete every returned variant inside existing database transactions. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `node --test tests/scoring.test.mjs tests/personal-backend.test.mjs tests/whatsapp-manager.test.mjs` Expected: PASS. ### Task 2: Sparse Verification Cache and Derived Lead State **Files:** - Modify: `src/db.mjs` - Modify: `src/treatment/queries.mjs` - Test: `tests/db.test.mjs` - Test: `tests/personal-backend.test.mjs` **Interfaces:** - Produces: `saveWhatsAppVerification(db, result): object` - Produces: `getWhatsAppVerification(db, numero): object` with `status: 'nao_consultado'` when absent - Produces on lead rows: `whatsapp_status`, `whatsapp_numero_resolvido`, `whatsapp_checked_at` - [ ] **Step 1: Write failing migration and helper tests** ```js saveWhatsAppVerification(db, { numero: '5575999091780', status: 'confirmado', numeroResolvido: '557599091780', variante: 'sem_nono_digito' }); assert.equal(getWhatsAppVerification(db, '557599091780').status, 'confirmado'); assert.equal(getWhatsAppVerification(db, '5511999990001').status, 'nao_consultado'); ``` Also insert a lead and assert `getLead` and `listLeads` expose the latest equivalent cached result. - [ ] **Step 2: Run focused tests and verify RED** Run: `node --test tests/db.test.mjs tests/personal-backend.test.mjs` Expected: FAIL because migration v14 and verification helpers do not exist. - [ ] **Step 3: Add migration v14 and helper queries** ```sql CREATE TABLE whatsapp_number_verifications ( numero TEXT PRIMARY KEY, status TEXT NOT NULL CHECK (status IN ('confirmado','nao_cadastrado','erro_temporario')), numero_resolvido TEXT, variante TEXT, erro TEXT, checked_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_whatsapp_verification_status ON whatsapp_number_verifications(status, checked_at DESC); ``` Use `equivalentPhoneNumbers(numero)` to read all variants and order by `checked_at DESC, rowid DESC`. Upsert only the requested normalized number. Extend lead list/detail projection without changing `telefone_valido`. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `node --test tests/db.test.mjs tests/personal-backend.test.mjs tests/dashboard.test.mjs` Expected: PASS. ### Task 3: Bounded Baileys Lookup and Send Operations **Files:** - Modify: `src/whatsapp/manager.mjs` - Test: `tests/whatsapp-manager.test.mjs` **Interfaces:** - `createWhatsAppManager` adds `lookupTimeoutMs = 10000` and `messageTimeoutMs = 30000` - `checkNumber(number)` returns `numero`, `status`, `registered`, `resolvedNumber`, `variant`, and `candidatesChecked` - `sendText(...)` returns `messageId`, `resolvedNumber`, and `variant` - Lookup timeout error has `statusCode = 504`, `code = 'WHATSAPP_LOOKUP_TIMEOUT'` - Resolution failures expose requested-number metadata so routes can persist `nao_cadastrado` or `erro_temporario` - Send timeout error has `statusCode = 504`, `code = 'WHATSAPP_SEND_TIMEOUT'`, `deliveryUnknown = true`, and resolved-number metadata - [ ] **Step 1: Write failing timeout and result-contract tests** Use unresolved provider promises with 10 ms configured limits. Assert lookup rejects with `WHATSAPP_LOOKUP_TIMEOUT`; assert send reserves once, finalizes once, rejects with `deliveryUnknown`, invalidates the uncertain socket, and prevents the next queue item from using that socket. - [ ] **Step 2: Run focused tests and verify RED** Run: `node --test tests/whatsapp-manager.test.mjs` Expected: FAIL because provider calls currently wait forever and results omit resolved metadata. - [ ] **Step 3: Implement a bounded provider helper** ```js function withTimeout(promise, timeoutMs, createError) { let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(createError()), timeoutMs); }); promise.catch(() => {}); return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); } ``` Apply it separately to `socket.onWhatsApp` and `socket.sendMessage`. A send timeout must invalidate the uncertain socket and enter the existing reconnect lifecycle before the queue can use a provider again. Preserve the pending-send counter, persistence callbacks, ACK race handling, and no-automatic-retry behavior. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `node --test tests/whatsapp-manager.test.mjs` Expected: PASS with no unhandled rejection or leaked timer warning. ### Task 4: Persist Checks and Honor the Visible Manual Destination **Files:** - Modify: `src/server/routes/whatsapp.mjs` - Modify: `src/server/routes/outreach.mjs` - Modify: `src/db.mjs` - Test: `tests/whatsapp-routes.test.mjs` - Test: `tests/outreach-routes.test.mjs` **Interfaces:** - `registerWhatsAppRoutes(router, { db, whatsapp, toDataURL })` - `finishOutreachSend(db, sendId, { messageId, error, unknown })` - Manual send response retains existing fields and may add resolved-number metadata - [ ] **Step 1: Replace the incorrect route expectation with failing destination tests** ```js assert.equal(sent[0].number, '5511999990001'); assert.equal(sent[0].message, 'Oi Empresa Real de Santos'); assert.equal(db.prepare('SELECT lead_id FROM outreach_sends').get().lead_id, 1); ``` Add route tests for confirmed, non-registered, and lookup-timeout cache writes. Add a send-timeout test asserting status `202`, `status: 'resultado_desconhecido'`, and database status `desconhecido`. - [ ] **Step 2: Run route tests and verify RED** Run: `node --test tests/whatsapp-routes.test.mjs tests/outreach-routes.test.mjs` Expected: FAIL because the lead phone currently overrides `body.numero` and routes do not persist verification. - [ ] **Step 3: Implement the minimal route changes** Set the destination with: ```js const rawNumber = String(body?.numero || '').trim(); const context = lead || clientContext; const message = renderTemplate(messageTemplate, { ...context, whatsapp: number }); ``` Persist check outcomes through `saveWhatsAppVerification`. For a lookup timeout save `erro_temporario` before returning 504. For a send timeout finalize the reserved row with `unknown: true` and return the non-retry warning. - [ ] **Step 4: Run route and integration tests and verify GREEN** Run: `node --test tests/whatsapp-routes.test.mjs tests/outreach-routes.test.mjs tests/server-whatsapp-integration.test.mjs` Expected: PASS. ### Task 5: Restrict Automatic Outreach to Confirmed Numbers **Files:** - Modify: `src/outreach/run.mjs` - Test: `tests/outreach.test.mjs` **Interfaces:** - Consumes: `whatsapp_number_verifications` and equivalent Brazilian variants - Produces: unchanged `runOutreachBatch` summary contract - [ ] **Step 1: Write failing candidate-selection tests** Insert otherwise identical leads in the four verification states. Assert only `confirmado` is passed to `sendText`. Add an alternate-variant confirmed record and assert it qualifies the stored lead number. - [ ] **Step 2: Run the focused test and verify RED** Run: `node --test tests/outreach.test.mjs` Expected: FAIL because current SQL selects every syntactically valid phone. - [ ] **Step 3: Add confirmed-verification selection** Select syntactically valid candidates, then retain only rows whose `getWhatsAppVerification(db, lead.whatsapp).status === 'confirmado'`. Keep the current daily limit, product dedupe, business hours, cancellation, and queue-time policy checks unchanged. - [ ] **Step 4: Run focused tests and verify GREEN** Run: `node --test tests/outreach.test.mjs tests/scheduler.test.mjs` Expected: PASS. ### Task 6: Correct UI Semantics, Polling, and Lead Context **Files:** - Modify: `public/index.html` - Modify: `public/js/app.js` - Modify: `public/js/tratamento.js` - Test: `tests/personal-branding.test.mjs` **Interfaces:** - `window.MaquinaSelecionarLeadManual(lead)` remains available - Add `window.MaquinaLimparLeadManual()` or an equivalent bound clear button - Lead API fields consumed: `whatsapp_status`, `whatsapp_numero_resolvido`, `whatsapp_checked_at` - [ ] **Step 1: Write failing static UI regressions** Assert the HTML and scripts contain `Telefone: todos`, `Com telefone`, a separate WhatsApp status label, a clear-context control, and stable-state polling. Assert misleading strings such as `WhatsApp valido` are absent from `tratamento.js`. - [ ] **Step 2: Run the UI test and verify RED** Run: `node --test tests/personal-branding.test.mjs` Expected: FAIL on current WhatsApp-as-phone labels and READY polling stop. - [ ] **Step 3: Implement UI copy and behavior** Use `telefone_valido` only for telephone labels and score text. Render WhatsApp verification independently from `lead.whatsapp_status`. Make `scheduleWhatsAppPolling` use approximately 1 second for transitional states and 5 seconds for stable states while the dialog remains open. The clear action sets `manualLeadContext = null` and rerenders context without clearing number or message. - [ ] **Step 4: Run UI and treatment tests and verify GREEN** Run: `node --test tests/personal-branding.test.mjs tests/dashboard.test.mjs` Expected: PASS. ### Task 7: End-to-End Verification **Files:** - Verify only; fix only regressions caused by Tasks 1-6 **Interfaces:** - No new interfaces - [ ] **Step 1: Run the complete test suite** Run: `npm test` Expected: all tests pass with no warnings or unhandled rejections. - [ ] **Step 2: Run the production dependency audit** Run: `npm audit --omit=dev` Expected: zero vulnerabilities. - [ ] **Step 3: Restart and smoke-test the local server** Verify `GET /api/whatsapp/status`, the settings dialog polling, lead selection, number editing, context clearing, and the check-number endpoint. Do not send an uncontrolled real message. - [ ] **Step 4: Inspect the final diff** Run: `git status --short` and `git diff --check`. Expected: only intended project files changed and no whitespace errors. Do not revert unrelated existing changes.