Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
322 lines
13 KiB
Markdown
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.
|