Baseline commit of the existing Maquina Comercial Pessoal project
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.
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user