diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..3e39a5d --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"b4e65d8e-b477-417b-9390-f3d14033ef91","pid":3138376,"procStart":"71699559","acquiredAt":1777673126273} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 52c49de..1f8e4a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,9 @@ Repetitive, hardcoded responses are a known quality problem and the focus of the ## Architecture -This is a **multi-tenant WhatsApp e-commerce chatbot** powered by Express.js. Tenants are WooCommerce store operators; their customers interact via WhatsApp to browse products, build carts, and place orders. All database operations are isolated by `tenant_id`. +This is a **mono-tenant WhatsApp e-commerce chatbot** powered by Express.js. The store operator hooks the bot to a single WooCommerce shop; customers interact via WhatsApp to browse products, build carts, and place orders. + +The DB schema retains `tenant_id` columns (it was originally multi-tenant) but the app boots with a single tenant resolved at startup. The single id is exposed via `src/modules/shared/tenant.js` (`getTenantId()`); webhook handlers and intake routes read from there instead of looking up tenants per-request. ### Request flow diff --git a/index.js b/index.js index ca4acfc..44cd325 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,10 @@ import "dotenv/config"; import { ensureTenant } from "./src/modules/2-identity/db/repo.js"; +import { setTenant } from "./src/modules/shared/tenant.js"; import { createApp } from "./src/app.js"; async function configureUndiciDispatcher() { - // Node 18+ usa undici debajo de fetch. Esto suele arreglar timeouts “fantasma” por keep-alive/pooling. - // Nota: si el módulo `undici` no está disponible, no rompemos el arranque (solo logueamos warning). + // Node 18+ usa undici debajo de fetch. Esto suele arreglar timeouts "fantasma" por keep-alive/pooling. try { const { setGlobalDispatcher, Agent } = await import("undici"); setGlobalDispatcher( @@ -21,21 +21,14 @@ async function configureUndiciDispatcher() { } } -/** - * --- Tenant --- - */ const TENANT_KEY = process.env.TENANT_KEY || "piaf"; -let TENANT_ID = null; - -/** - * --- Boot --- - */ const port = process.env.PORT || 3000; (async function boot() { await configureUndiciDispatcher(); - TENANT_ID = await ensureTenant({ key: TENANT_KEY, name: TENANT_KEY.toUpperCase() }); - const app = createApp({ tenantId: TENANT_ID }); + const tenantId = await ensureTenant({ key: TENANT_KEY, name: TENANT_KEY.toUpperCase() }); + setTenant({ id: tenantId, key: TENANT_KEY }); + const app = createApp({ tenantId }); app.listen(port, () => console.log(`UI: http://localhost:${port} (tenant=${TENANT_KEY})`)); })().catch((err) => { console.error("Boot failed:", err); diff --git a/src/modules/1-intake/handlers/evolution.js b/src/modules/1-intake/handlers/evolution.js index bee02d4..b41df84 100644 --- a/src/modules/1-intake/handlers/evolution.js +++ b/src/modules/1-intake/handlers/evolution.js @@ -1,29 +1,25 @@ import crypto from "crypto"; import { parseEvolutionWebhook } from "../services/evolutionParser.js"; -import { resolveTenantId, processMessage } from "../../2-identity/services/pipeline.js"; +import { processMessage } from "../../2-identity/services/pipeline.js"; +import { getTenantId } from "../../shared/tenant.js"; import { debug as dbg } from "../../shared/debug.js"; export async function handleEvolutionWebhook(body) { const t0 = Date.now(); const parsed = parseEvolutionWebhook(body); -if (!parsed.ok) { + if (!parsed.ok) { return { status: 200, payload: { ok: true, ignored: parsed.reason } }; } if (dbg.perf || dbg.evolution) { console.log("[perf] evolution.webhook.start", { - tenant_key: parsed.tenant_key || null, chat_id: parsed.chat_id, message_id: parsed.message_id || null, ts: parsed.ts || null, }); } - const tenantId = await resolveTenantId({ - chat_id: parsed.chat_id, - tenant_key: parsed.tenant_key, - to_phone: null, - }); + const tenantId = getTenantId(); const pm = await processMessage({ tenantId, @@ -33,7 +29,7 @@ if (!parsed.ok) { text: parsed.text, provider: "evolution", message_id: parsed.message_id || crypto.randomUUID(), - meta: { pushName: parsed.from_name, ts: parsed.ts, instance: parsed.tenant_key, source: parsed.source }, + meta: { pushName: parsed.from_name, ts: parsed.ts, source: parsed.source }, }); if (dbg.perf || dbg.evolution) { @@ -48,4 +44,3 @@ if (!parsed.ok) { return { status: 200, payload: { ok: true } }; } - diff --git a/src/modules/1-intake/handlers/sim.js b/src/modules/1-intake/handlers/sim.js index 987ad46..eef3881 100644 --- a/src/modules/1-intake/handlers/sim.js +++ b/src/modules/1-intake/handlers/sim.js @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { resolveTenantId } from "../../2-identity/services/pipeline.js"; import { processMessage } from "../../2-identity/services/pipeline.js"; +import { getTenantId } from "../../shared/tenant.js"; export async function handleSimSend(body) { const { chat_id, from_phone, text } = body || {}; @@ -10,11 +10,7 @@ export async function handleSimSend(body) { const provider = "sim"; const message_id = crypto.randomUUID(); - const tenantId = await resolveTenantId({ - chat_id, - tenant_key: body?.tenant_key, - to_phone: body?.to_phone, - }); + const tenantId = getTenantId(); const result = await processMessage({ tenantId, @@ -27,4 +23,3 @@ export async function handleSimSend(body) { return { status: 200, payload: { ok: true, run_id: result.run_id, reply: result.reply } }; } - diff --git a/src/modules/2-identity/controllers/wooWebhooks.js b/src/modules/2-identity/controllers/wooWebhooks.js index 11972b4..0a4dbbb 100644 --- a/src/modules/2-identity/controllers/wooWebhooks.js +++ b/src/modules/2-identity/controllers/wooWebhooks.js @@ -1,5 +1,5 @@ import { refreshProductByWooId } from "../../shared/wooSnapshot.js"; -import { getTenantByKey } from "../db/repo.js"; +import { getTenantId } from "../../shared/tenant.js"; import { insertAuditLog } from "../../0-ui/db/repo.js"; function unauthorized(res) { @@ -48,11 +48,8 @@ export function makeWooProductWebhook() { const { id, parentId, resource, action, changes } = parseWooPayload(req.body || {}); if (!id) return res.status(400).json({ ok: false, error: "missing_id" }); - // Determinar tenant por query ?tenant_key=... - const tenantKey = req.query?.tenant_key || process.env.TENANT_KEY || null; - if (!tenantKey) return res.status(400).json({ ok: false, error: "missing_tenant_key" }); - const tenant = await getTenantByKey(String(tenantKey).toLowerCase()); - if (!tenant?.id) return res.status(404).json({ ok: false, error: "tenant_not_found" }); + // Mono-tenant: el tenant es el único cargado al boot. + const tenant = { id: getTenantId() }; const parentForVariation = resource && String(resource).includes("variation") ? parentId || null : null; diff --git a/src/modules/2-identity/db/repo.js b/src/modules/2-identity/db/repo.js index 098c3a7..0ed024d 100644 --- a/src/modules/2-identity/db/repo.js +++ b/src/modules/2-identity/db/repo.js @@ -461,21 +461,6 @@ export async function deleteIdentityMapByChat({ tenant_id, wa_chat_id, provider return rowCount || 0; } -export async function getTenantByKey(key) { - const { rows } = await pool.query(`select id, key, name from tenants where key=$1`, [key]); - return rows[0] || null; -} - -export async function getTenantIdByChannel({ channel_type, channel_key }) { - const q = ` - select tenant_id - from tenant_channels - where channel_type=$1 and channel_key=$2 - `; - const { rows } = await pool.query(q, [channel_type, channel_key]); - return rows[0]?.tenant_id || null; -} - export async function getExternalCustomerIdByChat({ tenant_id, wa_chat_id, provider = "woo" }) { const q = ` select external_customer_id diff --git a/src/modules/2-identity/services/pipeline.js b/src/modules/2-identity/services/pipeline.js index 9210fb2..bbfdefb 100644 --- a/src/modules/2-identity/services/pipeline.js +++ b/src/modules/2-identity/services/pipeline.js @@ -8,9 +8,8 @@ import { getExternalCustomerIdByChat, upsertExternalCustomerMap, updateRunLatency, - getTenantByKey, - getTenantIdByChannel, } from "../db/repo.js"; +import { getTenantId } from "../../shared/tenant.js"; import { sseSend } from "../../shared/sse.js"; import { createWooCustomer, getWooCustomerById } from "./woo.js"; import { debug as dbg } from "../../shared/debug.js"; @@ -469,28 +468,11 @@ const runStatus = llmMeta?.error ? "warn" : "ok"; return { run_id, reply: plan.reply }; } -function parseTenantFromChatId(chat_id) { - const m = /^([a-z0-9_-]+):/.exec(chat_id); - return m?.[1]?.toLowerCase() || null; -} - -export async function resolveTenantId({ chat_id, to_phone = null, tenant_key = null }) { - const explicit = (tenant_key || parseTenantFromChatId(chat_id) || "").toLowerCase(); - - if (explicit) { - const t = await getTenantByKey(explicit); - if (t) return t.id; - throw new Error(`tenant_not_found: ${explicit}`); - } - - if (to_phone) { - const id = await getTenantIdByChannel({ channel_type: "whatsapp", channel_key: to_phone }); - if (id) return id; - } - - const fallbackKey = (process.env.TENANT_KEY || "piaf").toLowerCase(); - const t = await getTenantByKey(fallbackKey); - if (t) return t.id; - throw new Error(`tenant_not_found: ${fallbackKey}`); +/** + * Mono-tenant: devuelve el id resuelto al boot. No hace queries por turno. + * Se mantiene como async para no romper callers existentes. + */ +export async function resolveTenantId() { + return getTenantId(); } diff --git a/src/modules/shared/tenant.js b/src/modules/shared/tenant.js new file mode 100644 index 0000000..189ff64 --- /dev/null +++ b/src/modules/shared/tenant.js @@ -0,0 +1,30 @@ +/** + * Mono-tenant: el bot opera con un único tenant resuelto al boot. + * + * Toda la infra DB sigue con columnas `tenant_id` (queda lista para + * multi-tenant futuro) pero la app se inicializa con un único ID + * y todas las llamadas a "resolver el tenant" devuelven ese mismo. + * + * Setup en boot (index.js): `setTenantId(uuid)` después de ensureTenant. + * Lectura en cualquier capa: `getTenantId()`. + */ + +let _tenantId = null; +let _tenantKey = null; + +export function setTenant({ id, key }) { + if (!id) throw new Error("setTenant requires id"); + _tenantId = id; + _tenantKey = key || null; +} + +export function getTenantId() { + if (!_tenantId) { + throw new Error("tenant not initialized: call setTenant() at boot"); + } + return _tenantId; +} + +export function getTenantKey() { + return _tenantKey; +}