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