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

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_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

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): 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

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:

  • 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
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_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.