Files
COMERCIAL-GTI/maquina-comercial-pessoal/docs/superpowers/plans/2026-07-29-whatsapp-verification-and-manual-send.md
GustavoandClaude Sonnet 5 417da56bd6 chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: maquina-comercial-pessoal
git-subtree-mainline: f5af038e19
git-subtree-split: a9ef4d776d
2026-09-28 15:50:17 -03:00

322 lines
13 KiB
Markdown

# 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.