# Myristica — WhatsApp Bot + Admin Web App — Full Handover

## Project Overview

**Myristica** is a rental property management web app for a commercial lease at Arcos 1836, CABA (Argentina). It tracks rent payments, IPC adjustments, service bills, and penalties. A WhatsApp group bot sits on top of it so the family (owner + tenants) can query debts, register payments, send comprobantes, and receive auto-generated report images directly from WhatsApp.

**Repo:** https://github.com/ignaciolagosruiz/myristica
**Production URL:** https://myristica-eight.vercel.app
**Supabase project:** `kszwxklcerptpiacnypn` (region: us-east-1)
**Oracle VM:** `ubuntu@100.87.116.90` (Tailscale), SSH key at `C:\Users\ignac\OneDrive\Documents\dev\oracle\id_ed25519`
**Public bridge URL:** `https://miopenclaw-vnic.tail9799d2.ts.net/`
**Project path (local):** `C:\Users\ignac\OneDrive\Documents\claude\myristica\app`

---

## Architecture

```
WhatsApp Group
     ↕
Baileys 7.0 bridge (Oracle VM :8080, systemd: wa-bridge)
  ~/wa-bridge/index.js
     ↕ HTTP POST (x-api-key header)
Vercel — /api/webhook/whatsapp (Next.js 16.2.1 route handler)
     ↕                     ↕
Mistral API              Supabase DB (postgres)
  mistral-large-latest    - pagos (rent payments)
  mistral-ocr-latest      - gastos_servicios (service bills)
                          - pagos_servicios (service payments)
                          - servicios (ABL, AYSA, Destapaciones)
                          - bot_sessions (conversation state)
                          - contratos (id=3, but pagos use contrato_id=1)
                          - indices_ipc
                          - archivos (metadata only — files on Oracle VM)
                          - configuracion (key/value config store)

File storage: Oracle VM ~/wa-bridge/media/ (public at /media/<filename>)
Table rendering: satori (text→SVG paths) + sharp (SVG→PNG) with Geist font
```

---

## Deployment Workflow (IMPORTANT)

The project follows this flow:

### 1. Local Development & Testing
- Code in: `C:\Users\ignac\OneDrive\Documents\claude\myristica\app`
- Build check: `npm run build` (catches TypeScript errors)
- Bot testing: send messages to the WhatsApp group (live testing via the deployed bridge)
- Preview: `npm run dev` for web app UI changes

### 2. Push to GitHub
```bash
git add -A
git commit -m "descriptive message"
git push origin main
```

### 3. Verify Vercel Deployment
After pushing, wait ~1 minute, then verify:
```bash
# Check latest deployment
npx vercel ls 2>&1 | head -5

# Or visit the production URL
# https://myristica-eight.vercel.app
```

**Auto-deploy:** Vercel auto-deploys from `main` branch on push. If deployment doesn't trigger, run manually:
```bash
npx vercel --prod --yes
```

**Deploy webhook (optional):** To get notified when a deployment completes, a Vercel Deploy Hook can be set up at:
https://vercel.com/ignaciolagosruizs-projects/myristica/settings/git
This gives a webhook URL that can be called (e.g., from GitHub Actions or a cron check) to trigger/re-verify deploys.

### 4. Bot Bridge Updates
Bridge files live in `app/bridge/` locally. After changes, manually sync to Oracle VM:
```bash
scp -i "C:\Users\ignac\OneDrive\Documents\dev\oracle\id_ed25519" bridge/index.js ubuntu@100.87.116.90:~/wa-bridge/
ssh -i "C:\Users\ignac\OneDrive\Documents\dev\oracle\id_ed25519" ubuntu@100.87.116.90 "sudo systemctl restart wa-bridge"
```

---

## Infrastructure Details

### Oracle VM
- **Bridge file:** `~/wa-bridge/index.js`
- **Bridge package:** `~/wa-bridge/package.json`
- **Service:** `sudo systemctl restart wa-bridge` / `sudo systemctl status wa-bridge`
- **Media directory:** `~/wa-bridge/media/` (served publicly at `https://miopenclaw-vnic.tail9799d2.ts.net/media/<filename>`)
- **Tailscale Funnel:** `sudo tailscale funnel --bg 8080` (must be running for public access)
- **Bridge API key:** `myristica-evo-2026`
- **Node deps:** `@whiskeysockets/baileys`, `express`, `qrcode-terminal`
- **SSH:** `ssh -i "C:\Users\ignac\OneDrive\Documents\dev\oracle\id_ed25519" ubuntu@100.87.116.90`

### Bridge endpoints (all require `apikey: myristica-evo-2026` header except `/media/<file>`)

| Endpoint | Method | Description |
|---|---|---|
| `/` | GET | Health check |
| `/status` | GET | WhatsApp connection status |
| `/qr` | GET | QR code for re-linking |
| `/media/upload` | POST | Upload base64 file, returns `{ url, filename }` |
| `/media/:filename` | GET | Serve file publicly (no auth) |
| `/media/:filename` | DELETE | Delete file |
| `/message/sendText/myristica` | POST | Send text to WhatsApp |
| `/message/sendButtons/myristica` | POST | Send buttons (always falls back to numbered list) |
| `/message/sendMedia/myristica` | POST | Send image or document (`media`: URL, data URI, or base64; `mediatype`: "image"/"document") |
| `/chat/sendPresence/myristica` | POST | Set typing presence (`composing`/`paused`/`available`) |
| `/chat/getBase64FromMediaMessage/myristica` | POST | Download inbound WhatsApp media |
| `/group/fetchAllGroups/myristica` | GET | List groups |

**sendMedia details:**
- `media` field accepts: HTTP(S) URL, `data:mime;base64,...` data URI, or raw base64
- `mediatype: "image"` → sends as WhatsApp image with `jpegThumbnail: ""` (skip Baileys thumbnail crash)
- `mediatype: "document"` → sends as WhatsApp document with `fileName`
- Caption support: pass `caption` field to attach text to the image message

### Vercel Environment Variables
```
MISTRAL_API_KEY=...
EVOLUTION_API_URL=https://miopenclaw-vnic.tail9799d2.ts.net
EVOLUTION_API_KEY=myristica-evo-2026
EVOLUTION_INSTANCE_NAME=myristica
WHATSAPP_GROUP_JID=120363424714481067@g.us
BOT_WEBHOOK_SECRET=myristica-evo-2026
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
DATABASE_URL=...
```

### Supabase (Postgres)
- `configuracion` table stores app settings as key/value pairs
- `bot_watermark_preset` stores the current watermark configuration as JSON
- `acuerdo_monto` / `acuerdo_desde_periodo` store the verbal agreement rent

---

## Codebase Structure (bot + webapp)

```
src/
  app/
    api/webhook/whatsapp/route.ts        ← main webhook entry
    api/admin/
      config/route.ts                    ← getConfig/setConfig (configuracion table)
      pagos/route.ts                     ← pagos CRUD
      pagos/[id]/route.ts                ← single pago CRUD
      pagos-servicios/route.ts           ← pagos_servicios CRUD
      pagos-servicios/[id]/route.ts      ← single pago_servicio CRUD
      servicios/route.ts                 ← servicios CRUD
      servicios/[id]/route.ts            ← single servicio CRUD
      entries/[id]/route.ts              ← gastos_servicios entry CRUD
      archivos/route.ts                  ← file upload → Oracle VM
      archivos/[id]/route.ts             ← file delete → Oracle VM
      users/route.ts                     ← user management
      users/[id]/route.ts                ← single user management
      historial/route.ts                 ← audit history
    api/cron/
      rent-reminder/route.ts             ← rent reminder (cron disabled)
      ipc-reminder/route.ts              ← IPC alert (cron disabled)
    api/reportes-preview/[id]/route.ts   ← report image preview API
    reportes-preview/page.tsx            ← preview gallery page
    reportes-preview/WatermarkLab.tsx    ← interactive watermark tuning lab
    reportes-preview/ReportesPreviewClient.tsx ← gallery client component
    reportes-preview/WatermarkPreviewCanvas.tsx ← shared watermark preview canvas
    dashboard/                           ← dashboard with IPC + rent tables
    pagos/                               ← rent payment table (client component)
    servicios/                           ← service management (client component)
    ajustes-ipc/                         ← IPC adjustment + index tables
    historial/                           ← change history table
    usuarios/                            ← user management + scope matrix
    graficas/                            ← charts (summary + charts component)
  lib/
    db.ts                                ← Supabase postgres connection
    auth.ts                              ← NextAuth v5 (Credentials + JWT)
    schema.ts                            ← Drizzle schema (all tables)
    penalties.ts                         ← calcularResumen() — rent debt calculation
    ipc.ts                               ← calcularAjustesIpc() — IPC adjustment math
    contract.ts                          ← hardcoded contract constants (fallback)
    data.ts                              ← getContratoData(), getIpcData(), getAcuerdoVerbalFromDb()
    config.ts                            ← getConfig() / setConfig()
    sync-pagos-servicios.ts             ← redistributes service payments across gastos_servicios
    service-payment-meta.ts             ← hidden metadata in payment notes (proof URL, uploader, payment mode)
    table-utils.ts                      ← table sort/filter utilities
  lib/bot/
    evolution.ts         ← bridge API client (sendText, sendButtons, sendMedia, downloadMedia, uploadMedia, sendPresence)
    mistral.ts           ← Mistral chat + tool orchestrator + OCR
    tools.ts             ← tool definitions + executors (consultar_*, registrar_*, exportar_*, generar_reporte_tabla)
    reports.ts           ← deterministic report generators (8 report types)
    render-table.ts      ← satori + sharp table→PNG with watermark overlay
    sessions.ts          ← bot_sessions CRUD (state machine for payment flow)
    watermark-preset.ts  ← default watermark preset + type
    watermark-config.ts  ← load/save watermark preset from configuracion table
    watermark-fonts.ts   ← font resolution (Geist bundled, Google TTF, safe sources)
    handlers/
      text.ts            ← handle text messages (BOT prefix + @mention), table images, CSV detection, help menu
      image.ts           ← handle image/PDF (OCR → upload → payment flow)
      interactive.ts     ← handle button/text replies (state machine + numeric fallback)
  components/
    FileUpload.tsx       ← handles both Supabase and Oracle VM file URLs
    Navbar.tsx           ← top navigation
    StatusBadge.tsx      ← payment status badge
    (more in components/admin/)
```

---

## Contract & Business Logic

**Contract:** Arcos 1836, CABA — commercial lease
- Locadora: Gloria Ruiz Perkins
- Locatarios: Juan Andrés Gelly y Obes + Alfredo Cayetano Cogorno
- Period: 01/03/2025 – 28/02/2028
- Base rent: $3,500,000 ARS/month
- IPC adjustment: quarterly (Jun, Sep, Dec, Mar) using INDEC indices
- Penalty: 0.30%/day on unpaid amount (cap 30 days)
- Verbal agreement: $4,500,000/month from Mar 2026 (stored in `configuracion` table: `acuerdo_monto`, `acuerdo_desde_periodo`)

**Services:**
| Service | Tenant % | Notes |
|---|---|---|
| ABL | 33% | Tenant pays 1/3 |
| AYSA | 70% | Tenant pays 70% |
| Destapaciones | 100% | Tenant pays all |

**Payment distribution for services:**
`pagos_servicios` table holds payments. `syncPagosServicios()` (in `src/lib/sync-pagos-servicios.ts`) redistributes them chronologically across `gastos_servicios.monto_pagado`. Always call this after inserting into `pagos_servicios`. Specific service payments (paymentMode: "specific") only apply to the selected service via metadata parsing.

**CRITICAL — contratoId mismatch:**
`contratos.id = 3` in the DB, but all `pagos` rows have `contrato_id = 1`. The web app queries pagos without any contratoId filter. The bot must do the same. DO NOT filter pagos by `contrato.id`. This was a major bug causing the bot to show 10x inflated rent debt.

---

## Allowed WhatsApp Senders

| JID (participant field) | Name used with AI | Notes |
|---|---|---|
| `173104512892995` | Nacho | WhatsApp internal user ID (LID) |
| `41841823088688` | Nacho | Backup phone (LID) |
| `112974970794216` | Luli | WhatsApp internal user ID (LID) |
| `5491167336646` | Luli | Phone format fallback |
| `5491144378344` | Gloria | Phone format — may need LID update |
| `5491158215174` | Nacho | Phone format fallback |

**IMPORTANT:** Baileys multi-device may send internal user IDs (LIDs like `173104512892995`) instead of phone numbers as the `participant` field in group messages. If a family member's messages are silently ignored, check the logs for their actual JID and add it to `ALLOWED_SENDERS`.

Defined in `src/app/api/webhook/whatsapp/route.ts` as `ALLOWED_SENDERS`.

---

## Bot Flow

### Text messages
1. Message arrives at `/api/webhook/whatsapp`
2. Checked: correct group JID, not fromMe, sender in ALLOWED_SENDERS
3. If starts with `BOT ` (case-insensitive) or `@BOT` mention → `handleTextMessage`. Other `@` mentions (e.g. `@Luli`) are ignored.
4. **Help menu:** If query is empty or matches `menu/ayuda/help/comandos/opciones`, sends the help text directly (no Mistral call)
5. **Confirmation routing:** If query is a simple confirmation phrase (`ok/si/confirmar/dale/1`) AND the user has an active `bot_sessions` state (not `idle`), routes to `handleInteractiveTextReply` instead of Mistral. This fixes the "BOT OK doesn't remember what I'm confirming" bug.
6. Strips prefix, prepends `"<Name> pregunta: "`, sends to Mistral with tools
7. Shows typing indicator (refreshed every 4s) during processing
8. Mistral may call tools, then returns text + table reports

### Table/image delivery (1 query = 1 message)
9. **If `tableReports` present:** builds caption from the first table's actual data (real Supabase numbers), renders table to PNG via satori+sharp, sends as **single image message with data in caption**. Only the FIRST table report is sent — subsequent reports are ignored to enforce 1 message per query.
10. **If no table reports but text has inline markdown tables:** extracts tables, renders to PNG, sends first image with preceding text as caption.
11. **If plain text:** sends as text message.
12. **CSV detection:** If response contains `[CSV_FILE:<url>:<filename>]`, sends file as WhatsApp document attachment.
13. `mdToWhatsApp()` strips bold/italic markers before sending text.

### Image/Document messages
1. Same checks as above
2. Downloads full message payload (not just key) via bridge
3. Uploads to Oracle VM (/media/upload) concurrently with OCR
4. `ocrReceipt()` → extracts amount, date, bank, reference
5. Stores `imageUrl` (Oracle VM URL, not base64) in `bot_sessions` context
6. Asks user to confirm registration with buttons/numbered list
7. On confirm: calls `registrar_pago_alquiler` or `registrar_pago_servicio` tool, stores proof URL as hidden metadata

### Button/list replies + numbered fallback
- State machine in `bot_sessions`: `idle` → `awaiting_payment_type` → `awaiting_service_select` → `awaiting_confirm`
- Bridge always sends numbered text lists (buttons not supported on WhatsApp)
- Numbered replies (`1`, `2`, `3`) and text replies (`si`, `no`, `alquiler`, etc.) ARE handled via `handleInteractiveTextReply`
- Flow: text → `handleInteractiveTextReply` → detects numbered or keyword → maps to button ID → routes to `handleButtonReply`
- If reply doesn't match any option, bot responds with "No entendi esa opcion..."

### Bot Commands (from help menu)
**Consultas:**
- `BOT deuda de servicios` — service debt summary (image with data in caption)
- `BOT deuda de alquiler` — rent debt summary (image with data in caption)
- `BOT detalle [servicio]` — single service detail (e.g., `detalle ABL`)
- `BOT próximo ajuste IPC` — next IPC adjustment info

**Reportes (imagen):**
- `BOT reporte servicios` — service debt table
- `BOT reporte alquiler` — rent debt breakdown
- `BOT reporte cuotas impagas` — unpaid rent months
- `BOT reporte pagos alquiler` — rent payment history
- `BOT reporte pagos servicios` — service payment history
- `BOT reporte distribución servicios` — how payments hit services

**Acciones:**
- `BOT registrar pago alquiler` — register rent payment
- `BOT registrar pago servicio` — register service payment

**Otro:**
- `BOT` (empty) or `BOT menu` / `BOT ayuda` — show help
- `BOT exportar CSV [deuda servicios/alquiler/completo]` — export CSV
- `ok` / `cancelar` — confirm/cancel during payment flow
- Send a photo of a receipt to auto-register a payment

---

## Deterministic Report Engine

Phase 1 has a full deterministic report engine in `src/lib/bot/reports.ts`:

| Report ID | Purpose | Columns | Delivery |
|---|---|---|---|
| `deuda_servicios_resumen` | Compact service debt | Servicio, Deuda + Total | Image (2-col caption) |
| `deuda_servicios_detallada` | Monthly breakdown by service | Periodo, Servicio, Deuda + Total | Image (title-only caption) |
| `servicio_detalle` | Single service per-period | Periodo, Facturado, Corresponde, Pagado, Deuda, Estado | Image (title-only caption) |
| `deuda_alquiler_resumen` | Rent debt breakdown | Concepto, Monto + Total | Image (2-col caption) |
| `alquiler_cuotas_impagas` | Open rent months | Periodo, Esperado IPC, Pagado, Saldo, Dias mora, Penalidad | Image (title-only caption) |
| `alquiler_mensual_completo` | Full rent audit | Periodo, Esperado IPC, Acordado, Pagado, Dif. IPC, Dias mora, Penalidad, Estado | Image (title-only caption) |
| `pagos_alquiler_historial` | Rent payment history | Periodo, Fecha pago, Monto, Notas, Registrado por, Comprobante | Image (title-only caption) |
| `pagos_servicios_historial` | Service payment history | Fecha, Monto, Modo, Servicio objetivo, Registrado por, Comprobante | Image (title-only caption) |
| `pagos_servicios_distribucion` | How payments hit services | Servicio, Corresponde, Pagado, Deuda restante | Image (title-only caption) |

**Caption logic (`buildCaptionFromTable` in text.ts):**
- **2-column summary tables** (deuda_servicios_resumen, deuda_alquiler_resumen): caption shows all data as a list (e.g., `ABL: $123.456\nAYSA: $789.012\nTotal: $1.012.478`)
- **Multi-column tables** (detalle, historial, etc.): caption shows only title + date (image has the full detail)

**Route:**
- Mistral receives natural language request
- With system prompt instructions, it maps to `generar_reporte_tabla(tipo, filtros)` — called ONCE per query
- Tool returns `[[TABLE_REPORT:{"title":"...","watermarkTitle":"...","markdown":"..."}]]`
- `text.ts` extracts the FIRST report, renders it with the saved watermark preset, sends as image with caption
- Mistral's text response is stripped of any markdown tables to prevent hallucinated numbers

---

## Watermark System

The watermark is configurable through the report preview lab and persisted in the database:

**Lab access:** `https://myristica-eight.vercel.app/reportes-preview` (login required)

**Controls:**
- `Opacity` (0.03 - 0.40)
- `Pattern offset X` (-100 to 100)
- `Pattern offset Y` (-20 to 20)
- `Stamp gap X/Y`
- `Watermark font`
- `Guardar como watermark del bot` (persists to DB → bot uses it)
- `Cargar guardado` (loads current DB preset)

**Saved preset:** stored in `configuracion` table as `bot_watermark_preset` key (JSON).

**Supported fonts:**
- `Geist` (bundled TTF, always works)
- `Faculty Glyphic` (loaded from TTF on GitHub, safe for production)
- Other Google Fonts may work but could fail if fetched as woff2; the renderer has a fallback to Geist if any font fails

**Current default for production:**
- `Faculty Glyphic`
- `opacity: 0.12`
- `stampGapX: 10`
- `stampGapY: 18`
- `offsetX: 0`
- `offsetY: 0`

**Rendering path:**
1. `renderTableToImage()` loads saved preset from DB via `getSavedWatermarkPreset()`
2. Each option (opacity, gaps, offsets, font) merges with explicit overrides if provided
3. Table rendered via satori → PNG
4. Watermark overlay rendered via satori → PNG
5. Both composited via sharp
6. Result sent to WhatsApp as base64 image

---

## Table Rendering Pipeline

1. **Parse:** `parseMarkdownTable()` extracts Markdown table from response
2. **Total row injection:** For 2-column service debt tables (`Servicio | Deuda`), the renderer tries to append a `Total` row even if the AI put it in a separate line
3. **Render:** satori (React elements → SVG with path text) + sharp (SVG → PNG)
4. **Font:** Geist Regular TTF for table text; saved watermark font for overlay
5. **Watermark:** Generated via satori (not raw SVG `<text>`) so glyphs always render correctly in production
6. **Delivery:** Base64 PNG sent as `data:image/png;base64,...` to bridge `/message/sendMedia/myristica` with caption
7. **Fallback:** If image fails, plain text table sent instead

**Why satori instead of sharp SVG:** Vercel serverless has no system fonts. Sharp's librsvg shows all text as empty boxes (tofu). Satori converts text to SVG `<path>` elements using font glyph outlines.

---

## File Storage

**All files go to Oracle VM.**

- Upload: POST `https://miopenclaw-vnic.tail9799d2.ts.net/media/upload` with `{ base64, mimetype, filename }` and `apikey` header
- Served at: `https://miopenclaw-vnic.tail9799d2.ts.net/media/<filename>` (public)
- Filenames: if provided, used as-is; if omitted, UUID v4 generated
- Old Supabase-hosted files: still accessible via old Supabase URL

---

## Critical Bugs Already Fixed (do not reintroduce)

1. **Service debt formula:** must apply porcentaje — formula is `(facturado * pct/100) - montoPagado`. If porcentaje is ignored, debt is ~10x too high.
2. **Rent debt contratoId:** DO NOT filter pagos by contratoId. `contrato.id = 3` but all pagos have `contrato_id = 1`. Query all pagos with no WHERE clause.
3. **Markdown bold:** convert `**text**` → plain text (no asterisks) before sending to WhatsApp.
4. **Env vars:** always `.trim()` all `process.env` values (trailing newlines from Vercel CLI).
5. **Table images:** must use satori for table rendering (not sharp SVG). Sharp librsvg renders all text as empty boxes (tofu).
6. **Bridge sendMedia:** must set `jpegThumbnail: ""`. Baileys sharp thumbnail generation crashes on SVG-derived PNGs.
7. **Watermark rendering:** must use satori for the watermark overlay too, not raw SVG `<text>` nodes. Raw SVG text depends on runtime font support which is unreliable in production.
8. **URL double extension:** bridge upload endpoint used to produce `file.csv.csv`. Fix: strip any existing extension before appending.
9. **Media download payload:** the app must send the full message object to the bridge download endpoint, not just the key.
10. **Inbound PDF OCR:** the Mistral OCR endpoint needs `document_url` type for PDFs, not `image_url`.
11. **Bot hallucinated numbers:** Mistral used to generate its own markdown tables with fake numbers in the text response. Fixed by: (a) system prompt forbids inline tables when using `generar_reporte_tabla`, (b) `stripMarkdownTables()` in text handler removes any hallucinated tables, (c) caption is built from actual table report data, not Mistral text.
12. **Multiple images per query:** Mistral was calling `generar_reporte_tabla` multiple times. Fixed by: (a) system prompt says "call ONCE per query", (b) text handler only sends the first table report.
13. **Payment confirmation not remembered:** "BOT OK" was routed to Mistral instead of the interactive handler. Fixed by: (a) checking for simple confirmation phrases in webhook route, (b) routing to `handleInteractiveTextReply` when `bot_sessions` state is not `idle`.

---

## Changes Log — 2026-03-29

### Bot response format overhaul
- **Single message delivery:** Table image is now sent with the text as a caption (not separate messages). 1 query = 1 message.
- **Caption built from real data:** For 2-column summary tables, the caption shows actual Supabase numbers (e.g., `ABL: $123.456`). For multi-column tables, caption is title + date.
- **1 table per query:** Only the first `generar_reporte_tabla` result is sent. System prompt tells Mistral to call the tool once.

### Payment confirmation context
- Added `isConfirmationCommand()` — detects `ok/si/confirmar/dale/listo/yes/1`
- Webhook route checks if a "BOT <confirmation>" message should go to the interactive handler
- `handleTextMessage` has a redundant safety check for the same
- Now "BOT OK" during a pending payment confirmation works correctly

### Help menu
- `BOT`, `BOT menu`, `BOT ayuda`, `BOT help`, `BOT comandos`, `BOT opciones` show a help text
- Lists all available commands, reports, and actions
- Served directly (no Mistral call) — instant response
- Empty `BOT` (no query) also shows help instead of "¿En qué puedo ayudarte?"

### Watermark slider
- Pattern offset X range extended from -20/20 to -100/100 (both UI slider and server-side clamp)

### Files modified
- `src/lib/bot/handlers/text.ts` — rewritten: caption from table data, single message, help menu, confirmation routing
- `src/app/api/webhook/whatsapp/route.ts` — confirmation routing, senderPhone passed to handleTextMessage
- `src/lib/bot/mistral.ts` — system prompt: use generar_reporte_tabla ONCE, no inline tables
- `src/lib/bot/tools.ts` — consultar_servicios_pendientes description updated
- `src/app/reportes-preview/WatermarkLab.tsx` — offset X slider min/max
- `src/lib/bot/watermark-config.ts` — offset X clamp range

---

## Known Gaps / TODO

1. **Cron jobs disabled:** `vercel.json` is empty `{}`. Route handlers exist at `/api/cron/rent-reminder` and `/api/cron/ipc-reminder` but need cron schedule.
2. **Archivo expiry:** No file cleanup on Oracle VM. Files persist forever.
3. **Next.js middleware deprecation:** `src/middleware.ts` shows warning but still works. Should migrate to `proxy.ts` eventually.
4. **More report types (Phase 2):** not implemented yet:
   - `proximo_ajuste_ipc` (next IPC adjustment date + estimate)
   - `ajustes_ipc_completos` (full IPC adjustment history)
   - `indices_ipc_mensuales` (monthly IPC index table)
5. **Bridge manual sync:** bridge files are in `app/bridge/` but must be manually deployed to the Oracle VM. No automated sync exists.
6. **Vercel deploy webhook:** not set up yet. Can be configured at Vercel → Settings → Git → Deploy Hooks to get a webhook URL that triggers/re-verifies deploys.

---

## Previous Handover Files

- `HANDOVER.md` (this file — latest version)
- `CURRENT_HANDOVER.md` (older summary, 2026-03-26)
- `HANDOVER_24_MARZO_2026.md` (pre-bot, web app only)

---

## Testing

### Bot testing (live via WhatsApp)
Send messages to the WhatsApp group. The bot processes them via the bridge → Vercel webhook → Mistral.

### Bridge health check
```bash
curl -s -H "apikey: myristica-evo-2026" "https://miopenclaw-vnic.tail9799d2.ts.net/status"
```

### Bridge logs (SSH)
```bash
ssh -i "C:\Users\ignac\OneDrive\Documents\dev\oracle\id_ed25519" ubuntu@100.87.116.90 "journalctl -u wa-bridge -n 30 --no-pager"
```

### Build check
```bash
cd C:\Users\ignac\OneDrive\Documents\claude\myristica\app
npm run build
```

### ADB (Pixel 9a)
```bash
# Screenshot (Git Bash)
MSYS_NO_PATHCONV=1 adb shell "screencap -d <display_id> /sdcard/screen.png" && adb pull //sdcard/screen.png screen.png
```

---

## Continuation Prompt for the Next Model

```
You are continuing development of the Myristica WhatsApp bot.

The project is a Next.js 16.2.1 app deployed on Vercel that manages a commercial
rental property at Arcos 1836, Buenos Aires. A WhatsApp group bot lets the family
(owner + tenants) query debts, register payments, send comprobantes, and receive
auto-generated report images.

Read HANDOVER.md in the repo root for full context.

DEPLOYMENT WORKFLOW:
1. Local: edit code, run `npm run build` to check for errors
2. GitHub: `git add -A && git commit -m "msg" && git push origin main`
3. Vercel: auto-deploys from main. Verify after ~1 min with `npx vercel ls`
4. Bridge: manual sync to Oracle VM if bridge files changed

KEY FACTS:
- Vercel (Next.js 16.2.1) ← webhook from Baileys bridge on Oracle VM ← WhatsApp
- Mistral AI (mistral-large-latest) for chat + tool calls; mistral-ocr-latest for receipts
- Supabase Postgres for all data; Oracle VM for all file storage + bridge
- Bridge URL: https://miopenclaw-vnic.tail9799d2.ts.net/  API key: myristica-evo-2026
- Table rendering: satori (React→SVG paths) + sharp (SVG→PNG) with Geist font
- CSV export: tool generates CSV, uploads to Oracle VM, sends as WhatsApp document
- Watermark: configurable via /reportes-preview; persisted in configuracion table
  as bot_watermark_preset key; satori overlay rendered (not raw SVG text)
- 1 query = 1 message: only first table report sent, caption built from real data
- Confirmation routing: "BOT OK" during active session routes to interactive handler
- Help menu: "BOT" / "BOT menu" / "BOT ayuda" shows commands (no Mistral call)

CRITICAL BUGS ALREADY FIXED (do not reintroduce):
1. Service debt: must apply porcentaje — formula is (facturado * pct/100) - montoPagado
2. Rent debt: DO NOT filter pagos by contratoId — all pagos have contrato_id=1 but
   contratos.id=3 in DB. Query all pagos with no WHERE clause.
3. Markdown bold: strip asterisks before sending to WhatsApp — no bold in messages.
4. Env vars: always .trim() all process.env values.
5. Table images: must use satori for watermark rendering. Raw SVG <text> causes tofu
   in production because sharp/librsvg doesn't honor @font-face reliably.
6. Bridge sendMedia: must set jpegThumbnail: "". Baileys crashes on SVG-derived PNGs.
7. Watermark font: woff2 from Google is not supported by satori. Use TTF sources only.
8. Numbered list replies: handled via handleInteractiveTextReply. Keywords like si/no
   and numeric replies map to button IDs via mapTextReplyToButtonId.
9. Media download: send full message payload, not just key.
10. PDF OCR: use document_url type, not image_url.
11. Bot hallucinated numbers: system prompt + stripMarkdownTables() + caption from data.
12. Multiple images: only first table report sent, system prompt says call tool ONCE.
13. Payment confirmation: "BOT OK" routed to interactive handler when session active.

PROJECT STRUCTURE:
- src/lib/bot/ — bot core (mistral.ts, tools.ts, reports.ts, render-table.ts, handlers/)
- src/lib/bot/handlers/text.ts — text handler: help menu, confirmation routing, caption from table data
- src/lib/bot/reports.ts — deterministic report engine (8 reports)
- src/lib/bot/watermark-config.ts — load/save watermark preset from DB
- src/lib/bot/watermark-fonts.ts — font resolution (Geist bundled, Google TTF, safe sources)
- src/app/reportes-preview/ — watermark lab + preview gallery
- src/app/api/webhook/whatsapp/route.ts — webhook entry, ALLOWED_SENDERS, confirmation routing
- bridge/ — live Oracle VM bridge files (deployed manually)

WHAT'S NEXT (user wants these done):
1. More report types: proximo_ajuste_ipc, ajustes_ipc_completos, indices_ipc_mensuales
2. File expiry or cleanup on Oracle VM
3. Cron jobs re-enable (vercel.json)
4. Migrate middleware to proxy.ts
5. Vercel deploy webhook for deployment verification

The web app's calculation logic (penalties.ts, ipc.ts) is the source of truth.
The bot tools must mirror the web app exactly. When in doubt, read the web app code first.
```

---

*Updated: 2026-03-29 — Bot response format overhaul, confirmation routing, help menu, watermark slider*
