local dev setup + OPENAI_BASE_URL support + dashboard fix
- CLAUDE.md con arquitectura y comandos del proyecto - env.example: agregar LIMIT_CONVERSATIONS, MAX_CHARS_PER_MESSAGE, OPENAI_BASE_URL - docker-compose.override: puerto 3001, extra_hosts para modelo local en Linux - OpenAI clients: soporte OPENAI_BASE_URL para apuntar a modelo local compatible - stats.js: sync de órdenes en background, dashboard no bloquea al cargar - package-lock: dbmate movido a prod dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
91
CLAUDE.md
Normal file
91
CLAUDE.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start with nodemon auto-reload
|
||||||
|
npm start # Production start
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm test # Run all tests once (vitest run)
|
||||||
|
npm run test:watch # Watch mode
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
npx vitest run src/modules/3-turn-engine/orderModel.test.js
|
||||||
|
|
||||||
|
# Database migrations (requires DATABASE_URL in .env)
|
||||||
|
npm run migrate:up
|
||||||
|
npm run migrate:down
|
||||||
|
npm run migrate:redo
|
||||||
|
npm run migrate:status
|
||||||
|
npm run seed # Seed a tenant via scripts/seed-tenant.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
No lint command is configured.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
### Request flow
|
||||||
|
|
||||||
|
```
|
||||||
|
WhatsApp → Evolution API webhook → /webhook/evolution
|
||||||
|
↓
|
||||||
|
1-intake: route & normalize message
|
||||||
|
↓
|
||||||
|
3-turn-engine: NLU → FSM → state handler
|
||||||
|
↓
|
||||||
|
Response persisted to DB + sent back via Evolution API
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module structure (numbered layers)
|
||||||
|
|
||||||
|
- **`src/modules/0-UI/`** — Admin dashboard: REST controllers for products, conversations, settings, prompts, takeovers, recommendations, aliases. Each controller has a `db/` sub-layer for persistence.
|
||||||
|
|
||||||
|
- **`src/modules/1-intake/`** — Message ingestion. Routes: `/simulator` (dev UI), `/webhook/evolution` (WhatsApp). Normalizes incoming messages before passing to turn engine.
|
||||||
|
|
||||||
|
- **`src/modules/2-identity/`** — Tenant and user management. Maps WhatsApp numbers to WooCommerce customers. Stores encrypted WooCommerce credentials per tenant in `tenant_ecommerce_config`. Routes WooCommerce webhooks.
|
||||||
|
|
||||||
|
- **`src/modules/3-turn-engine/`** — Core logic. NLU classifies intents; FSM transitions states (`IDLE → CART → SHIPPING → PAYMENT → WAITING_WEBHOOKS`). Two NLU versions controlled by `USE_MODULAR_NLU` env flag. Two turn engine versions controlled by `TURN_ENGINE` env flag. State handlers map to FSM states.
|
||||||
|
|
||||||
|
- **`src/modules/4-woo-orders/`** — WooCommerce order sync. Fetches and caches customer order history for conversation context.
|
||||||
|
|
||||||
|
- **`src/modules/shared/`** — DB pool (PostgreSQL via `pg`), SSE for real-time admin UI updates, WooSnapshot (product catalog cache), debug utilities.
|
||||||
|
|
||||||
|
### Key integrations
|
||||||
|
|
||||||
|
| System | Purpose | Config |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| OpenAI | NLU intent classification & response generation | `OPENAI_API_KEY`, `OPENAI_MODEL` |
|
||||||
|
| Evolution API | WhatsApp send/receive | `EVOLUTION_API_URL`, `EVOLUTION_API_KEY`, `EVOLUTION_INSTANCE_NAME`, `EVOLUTION_SEND_ENABLED` |
|
||||||
|
| WooCommerce REST API | Products, orders, customers | `WOO_*` env vars or per-tenant in DB |
|
||||||
|
| PostgreSQL | Primary database | `DATABASE_URL` |
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
Migrations live in `db/migrations/` as timestamped SQL files managed by `dbmate`. Key tables:
|
||||||
|
- `tenants`, `tenant_config`, `tenant_settings`, `tenant_ecommerce_config`, `tenant_channels`
|
||||||
|
- `wa_identity_map` — WhatsApp ↔ WooCommerce customer mapping
|
||||||
|
- `wa_conversation_state` — FSM state + context per conversation
|
||||||
|
- `wa_messages` — Message history
|
||||||
|
- `woo_products_snapshot` — Cached product catalog
|
||||||
|
- `prompt_templates` — Versioned LLM prompts
|
||||||
|
- `human_takeovers`, `audit_log`, `conversation_runs`
|
||||||
|
|
||||||
|
### Feature flags (env vars)
|
||||||
|
|
||||||
|
- `TURN_ENGINE=v1|v2` — Which turn engine version to use
|
||||||
|
- `USE_MODULAR_NLU=1` — Use modular NLU (prompt templates from DB) vs. v3 hardcoded
|
||||||
|
- `EVOLUTION_SEND_ENABLED=1` — Actually send messages to WhatsApp (disable in dev/test)
|
||||||
|
- `DEBUG_PERF`, `DEBUG_WOO_HTTP`, `DEBUG_LLM`, `DEBUG_EVOLUTION` — Granular debug logging
|
||||||
|
|
||||||
|
### Local development
|
||||||
|
|
||||||
|
Copy `env.example` to `.env` and fill in values. Use `docker-compose.override.yaml` for local overrides. Run `docker compose up` to start app + Postgres + Redis. The Dockerfile runs migrations automatically on startup (`migrate:up && seed && start`).
|
||||||
|
|
||||||
|
Test files use Vitest with `globals: true` — no need to import `describe`, `it`, `expect`.
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3001:3000"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -13,3 +13,5 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- .:/usr/src/app
|
- .:/usr/src/app
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ EVOLUTION_API_KEY=your-api-key
|
|||||||
EVOLUTION_INSTANCE_NAME=piaf
|
EVOLUTION_INSTANCE_NAME=piaf
|
||||||
EVOLUTION_SEND_ENABLED=0 # 0=solo BD (pruebas), 1=envía a WhatsApp (producción)
|
EVOLUTION_SEND_ENABLED=0 # 0=solo BD (pruebas), 1=envía a WhatsApp (producción)
|
||||||
|
|
||||||
|
# ===================
|
||||||
|
# Limits
|
||||||
|
# ===================
|
||||||
|
LIMIT_CONVERSATIONS=100
|
||||||
|
MAX_CHARS_PER_MESSAGE=4000
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# Debug Flags (1/true/yes/on para activar)
|
# Debug Flags (1/true/yes/on para activar)
|
||||||
# ===================
|
# ===================
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
|
"dbmate": "^2.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"mysql2": "^3.16.2",
|
"mysql2": "^3.16.2",
|
||||||
@@ -22,7 +23,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"dbmate": "^2.0.0",
|
|
||||||
"nodemon": "^3.0.3",
|
"nodemon": "^3.0.3",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -108,7 +107,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -122,7 +120,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -136,7 +133,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -150,7 +146,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -164,7 +159,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -178,7 +172,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1466,7 +1459,6 @@
|
|||||||
"version": "2.28.0",
|
"version": "2.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/dbmate/-/dbmate-2.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/dbmate/-/dbmate-2.28.0.tgz",
|
||||||
"integrity": "sha512-kbJ+Aqna/SOsS86RuimX8X/qmo9ItG00EYMUfUQmo3xGoXBac4+PZwOYV0fVPIGakIRljNolhfJySkPAFjmZsA==",
|
"integrity": "sha512-kbJ+Aqna/SOsS86RuimX8X/qmo9ItG00EYMUfUQmo3xGoXBac4+PZwOYV0fVPIGakIRljNolhfJySkPAFjmZsA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"dbmate": "dist/cli.js"
|
"dbmate": "dist/cli.js"
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import * as ordersRepo from "../../4-woo-orders/ordersRepo.js";
|
|||||||
* Obtiene estadísticas de pedidos para el dashboard
|
* Obtiene estadísticas de pedidos para el dashboard
|
||||||
*/
|
*/
|
||||||
export async function handleGetOrderStats({ tenantId }) {
|
export async function handleGetOrderStats({ tenantId }) {
|
||||||
// 1. Sincronizar pedidos nuevos de Woo
|
// Sync en background — no bloqueamos el request
|
||||||
const syncResult = await syncOrdersIncremental({ tenantId });
|
const syncPromise = syncOrdersIncremental({ tenantId }).catch(err =>
|
||||||
|
console.error("[stats] sync error:", err)
|
||||||
// 2. Obtener todas las estadísticas en paralelo
|
);
|
||||||
|
|
||||||
|
// Respondemos con lo que hay en DB mientras sincroniza
|
||||||
const [monthlyStats, productStats, yoyStats, totals] = await Promise.all([
|
const [monthlyStats, productStats, yoyStats, totals] = await Promise.all([
|
||||||
ordersRepo.getMonthlyStats({ tenantId }),
|
ordersRepo.getMonthlyStats({ tenantId }),
|
||||||
ordersRepo.getProductStats({ tenantId }),
|
ordersRepo.getProductStats({ tenantId }),
|
||||||
@@ -36,8 +38,8 @@ export async function handleGetOrderStats({ tenantId }) {
|
|||||||
// YoY
|
// YoY
|
||||||
yoy: yoyStats,
|
yoy: yoyStats,
|
||||||
|
|
||||||
// Info de sync
|
// Info de sync (sincronizando en background)
|
||||||
synced: syncResult.synced,
|
synced: 0,
|
||||||
total_in_cache: syncResult.total,
|
total_in_cache: totals.total_orders ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ function getClient() {
|
|||||||
throw new Error("OPENAI_API_KEY is not set");
|
throw new Error("OPENAI_API_KEY is not set");
|
||||||
}
|
}
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
_client = new OpenAI({ apiKey });
|
const baseURL = process.env.OPENAI_BASE_URL || undefined;
|
||||||
|
_client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
||||||
}
|
}
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ function getClient() {
|
|||||||
throw new Error("OPENAI_API_KEY is not set");
|
throw new Error("OPENAI_API_KEY is not set");
|
||||||
}
|
}
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
_client = new OpenAI({ apiKey });
|
const baseURL = process.env.OPENAI_BASE_URL || undefined;
|
||||||
|
_client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
||||||
}
|
}
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ function getClient() {
|
|||||||
}
|
}
|
||||||
if (_client && _clientKey === apiKey) return _client;
|
if (_client && _clientKey === apiKey) return _client;
|
||||||
_clientKey = apiKey;
|
_clientKey = apiKey;
|
||||||
_client = new OpenAI({ apiKey });
|
const baseURL = process.env.OPENAI_BASE_URL || undefined;
|
||||||
|
_client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
||||||
return _client;
|
return _client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user