Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
13 KiB
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_validomeans syntactic phone validity only.- Automatic outreach uses only persisted
confirmadonumbers. - 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
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
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): objectwithstatus: 'nao_consultado'when absent -
Produces on lead rows:
whatsapp_status,whatsapp_numero_resolvido,whatsapp_checked_at -
Step 1: Write failing migration and helper tests
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
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:
-
createWhatsAppManageraddslookupTimeoutMs = 10000andmessageTimeoutMs = 30000 -
checkNumber(number)returnsnumero,status,registered,resolvedNumber,variant, andcandidatesChecked -
sendText(...)returnsmessageId,resolvedNumber, andvariant -
Lookup timeout error has
statusCode = 504,code = 'WHATSAPP_LOOKUP_TIMEOUT' -
Resolution failures expose requested-number metadata so routes can persist
nao_cadastradoorerro_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
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
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:
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_verificationsand equivalent Brazilian variants -
Produces: unchanged
runOutreachBatchsummary 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.