diff --git a/db/migrations/20260203123701_drop_mp_payments.sql b/db/migrations/20260203123701_drop_mp_payments.sql
new file mode 100644
index 0000000..848c9f0
--- /dev/null
+++ b/db/migrations/20260203123701_drop_mp_payments.sql
@@ -0,0 +1,21 @@
+-- migrate:up
+-- Eliminar la tabla mp_payments (integración de MercadoPago removida)
+drop table if exists mp_payments;
+
+-- migrate:down
+-- Recrear la tabla si se necesita rollback
+create table if not exists mp_payments (
+ tenant_id uuid not null references tenants(id) on delete cascade,
+ woo_order_id bigint null,
+ preference_id text null,
+ payment_id text null,
+ status text null,
+ paid_at timestamptz null,
+ raw jsonb not null default '{}'::jsonb,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now(),
+ primary key (tenant_id, payment_id)
+);
+
+create index if not exists mp_payments_tenant_order_idx
+ on mp_payments (tenant_id, woo_order_id);
diff --git a/db/migrations/20260204155247_delivery_zones.sql b/db/migrations/20260204155247_delivery_zones.sql
new file mode 100644
index 0000000..98bb674
--- /dev/null
+++ b/db/migrations/20260204155247_delivery_zones.sql
@@ -0,0 +1,7 @@
+-- migrate:up
+-- Agregar columna delivery_zones para configurar zonas de entrega por barrio CABA
+ALTER TABLE tenant_settings
+ADD COLUMN IF NOT EXISTS delivery_zones JSONB DEFAULT '{}';
+
+-- migrate:down
+ALTER TABLE tenant_settings DROP COLUMN IF EXISTS delivery_zones;
diff --git a/package-lock.json b/package-lock.json
index 0372d54..ee666ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3163,9 +3163,9 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
- "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
+ "version": "7.19.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.1.tgz",
+ "integrity": "sha512-Gpq0iNm5M6cQWlyHQv9MV+uOj1jWk7LpkoE5vSp/7zjb4zMdAcUD+VL5y0nH4p9EbUklq00eVIIX/XcDHzu5xg==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
diff --git a/public/components/orders-crud.js b/public/components/orders-crud.js
index 576ca59..5d898ab 100644
--- a/public/components/orders-crud.js
+++ b/public/components/orders-crud.js
@@ -472,7 +472,7 @@ class OrdersCrud extends HTMLElement {
container.querySelectorAll("tr[data-order-id]").forEach(row => {
row.onclick = () => {
const orderId = parseInt(row.dataset.orderId);
- const order = this.orders.find(o => o.id === orderId);
+ const order = this.orders.find(o => o.id == orderId);
if (order) {
this.selectOrder(order);
}
diff --git a/public/components/settings-crud.js b/public/components/settings-crud.js
index f1a023e..60eef2b 100644
--- a/public/components/settings-crud.js
+++ b/public/components/settings-crud.js
@@ -1,13 +1,27 @@
import { api } from "../lib/api.js";
const DAYS = [
- { id: "lun", label: "Lunes", short: "Lun" },
- { id: "mar", label: "Martes", short: "Mar" },
- { id: "mie", label: "Miércoles", short: "Mié" },
- { id: "jue", label: "Jueves", short: "Jue" },
- { id: "vie", label: "Viernes", short: "Vie" },
- { id: "sab", label: "Sábado", short: "Sáb" },
- { id: "dom", label: "Domingo", short: "Dom" },
+ { id: "lun", label: "Lunes", short: "L" },
+ { id: "mar", label: "Martes", short: "M" },
+ { id: "mie", label: "Miércoles", short: "X" },
+ { id: "jue", label: "Jueves", short: "J" },
+ { id: "vie", label: "Viernes", short: "V" },
+ { id: "sab", label: "Sábado", short: "S" },
+ { id: "dom", label: "Domingo", short: "D" },
+];
+
+// Lista oficial de 48 barrios de CABA
+const CABA_BARRIOS = [
+ "Agronomía", "Almagro", "Balvanera", "Barracas", "Belgrano", "Boedo",
+ "Caballito", "Chacarita", "Coghlan", "Colegiales", "Constitución",
+ "Flores", "Floresta", "La Boca", "La Paternal", "Liniers",
+ "Mataderos", "Monte Castro", "Montserrat", "Nueva Pompeya", "Núñez",
+ "Palermo", "Parque Avellaneda", "Parque Chacabuco", "Parque Chas",
+ "Parque Patricios", "Puerto Madero", "Recoleta", "Retiro", "Saavedra",
+ "San Cristóbal", "San Nicolás", "San Telmo", "Vélez Sársfield", "Versalles",
+ "Villa Crespo", "Villa del Parque", "Villa Devoto", "Villa General Mitre",
+ "Villa Lugano", "Villa Luro", "Villa Ortúzar", "Villa Pueyrredón",
+ "Villa Real", "Villa Riachuelo", "Villa Santa Rita", "Villa Soldati", "Villa Urquiza"
];
class SettingsCrud extends HTMLElement {
@@ -113,6 +127,62 @@ class SettingsCrud extends HTMLElement {
}
.min-order-field { margin-top:16px; padding-top:16px; border-top:1px solid #1e2a3a; }
+
+ /* Zonas de entrega */
+ .zones-search { margin-bottom:12px; }
+ .zones-search input {
+ width:100%; padding:10px 14px;
+ background:#0f1520 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236c7a89'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 12px center;
+ background-size:18px; padding-left:38px;
+ }
+ .zones-list { max-height:400px; overflow-y:auto; display:flex; flex-direction:column; gap:4px; }
+ .zone-row {
+ display:grid;
+ grid-template-columns:32px 1fr;
+ gap:12px;
+ align-items:start;
+ padding:10px 12px;
+ background:#0f1520;
+ border-radius:8px;
+ border:1px solid #1e2a3a;
+ transition:border-color .2s;
+ }
+ .zone-row.active { border-color:#1f6feb; background:#0f1825; }
+ .zone-row.hidden { display:none; }
+ .zone-toggle {
+ width:32px; height:18px; background:#253245; border-radius:9px;
+ cursor:pointer; position:relative; transition:background .2s; margin-top:2px;
+ }
+ .zone-toggle.active { background:#2ecc71; }
+ .zone-toggle::after {
+ content:''; position:absolute; top:2px; left:2px;
+ width:14px; height:14px; background:#fff; border-radius:50%;
+ transition:transform .2s;
+ }
+ .zone-toggle.active::after { transform:translateX(14px); }
+ .zone-content { display:flex; flex-direction:column; gap:8px; }
+ .zone-name { font-size:14px; color:#e7eef7; font-weight:500; }
+ .zone-config { display:none; gap:16px; flex-wrap:wrap; align-items:center; }
+ .zone-row.active .zone-config { display:flex; }
+ .zone-days { display:flex; gap:4px; }
+ .zone-day {
+ width:28px; height:28px; border-radius:6px;
+ background:#253245; color:#8aa0b5;
+ display:flex; align-items:center; justify-content:center;
+ font-size:11px; font-weight:600; cursor:pointer;
+ transition:all .15s;
+ }
+ .zone-day.active { background:#1f6feb; color:#fff; }
+ .zone-day:hover { background:#2d3e52; }
+ .zone-day.active:hover { background:#1a5fd0; }
+ .zone-cost { display:flex; align-items:center; gap:6px; }
+ .zone-cost label { font-size:12px; color:#8aa0b5; }
+ .zone-cost input { width:90px; padding:6px 10px; font-size:13px; text-align:right; }
+ .zones-summary {
+ margin-top:12px; padding:12px; background:#0f1520;
+ border-radius:8px; font-size:13px; color:#8aa0b5;
+ }
+ .zones-summary strong { color:#e7eef7; }
@@ -138,6 +208,10 @@ class SettingsCrud extends HTMLElement {
if (!this.settings.schedule) {
this.settings.schedule = { delivery: {}, pickup: {} };
}
+ // Asegurar que delivery_zones existe
+ if (!this.settings.delivery_zones) {
+ this.settings.delivery_zones = {};
+ }
this.loading = false;
this.render();
} catch (e) {
@@ -201,6 +275,72 @@ class SettingsCrud extends HTMLElement {
}).join("");
}
+ // Convierte nombre de barrio a key (ej: "Villa Crespo" -> "villa_crespo")
+ barrioToKey(name) {
+ return name.toLowerCase()
+ .normalize("NFD").replace(/[\u0300-\u036f]/g, "") // quitar acentos
+ .replace(/\s+/g, "_");
+ }
+
+ getZoneConfig(barrioKey) {
+ return this.settings?.delivery_zones?.[barrioKey] || null;
+ }
+
+ setZoneConfig(barrioKey, config) {
+ if (!this.settings.delivery_zones) {
+ this.settings.delivery_zones = {};
+ }
+ if (config === null) {
+ delete this.settings.delivery_zones[barrioKey];
+ } else {
+ this.settings.delivery_zones[barrioKey] = config;
+ }
+ }
+
+ renderZonesList() {
+ return CABA_BARRIOS.map(barrio => {
+ const key = this.barrioToKey(barrio);
+ const config = this.getZoneConfig(key);
+ const isActive = config?.enabled === true;
+ const days = config?.days || [];
+ const cost = config?.delivery_cost || 0;
+
+ return `
+
+
+
+
${barrio}
+
+
+ ${DAYS.map(d => `
+
${d.short}
+ `).join("")}
+
+
+
+
+
+
+
+
+ `;
+ }).join("");
+ }
+
+ renderZonesSummary() {
+ const zones = this.settings?.delivery_zones || {};
+ const activeZones = Object.entries(zones).filter(([k, v]) => v?.enabled);
+
+ if (activeZones.length === 0) {
+ return `
No hay zonas de entrega configuradas. Activá los barrios donde hacés delivery.
`;
+ }
+
+ return `
${activeZones.length} zona${activeZones.length > 1 ? 's' : ''} de entrega activa${activeZones.length > 1 ? 's' : ''}
`;
+ }
+
render() {
const content = this.shadowRoot.getElementById("content");
@@ -290,6 +430,24 @@ class SettingsCrud extends HTMLElement {
+
+
+
+
+ Zonas de Entrega (Barrios CABA)
+
+
+
+
+
+
+
+ ${this.renderZonesList()}
+
+
+ ${this.renderZonesSummary()}
+
+
@@ -359,6 +517,73 @@ class SettingsCrud extends HTMLElement {
// Reset button
this.shadowRoot.getElementById("resetBtn")?.addEventListener("click", () => this.load());
+
+ // Zone search
+ this.shadowRoot.getElementById("zoneSearch")?.addEventListener("input", (e) => {
+ const query = e.target.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+ this.shadowRoot.querySelectorAll(".zone-row").forEach(row => {
+ const barrio = row.dataset.barrio;
+ const barrioName = CABA_BARRIOS.find(b => this.barrioToKey(b) === barrio) || "";
+ const normalized = barrioName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+ row.classList.toggle("hidden", query && !normalized.includes(query));
+ });
+ });
+
+ // Zone toggles
+ this.shadowRoot.querySelectorAll(".zone-toggle").forEach(toggle => {
+ toggle.addEventListener("click", () => {
+ const barrio = toggle.dataset.barrio;
+ const config = this.getZoneConfig(barrio);
+
+ if (config?.enabled) {
+ // Desactivar zona
+ this.setZoneConfig(barrio, null);
+ } else {
+ // Activar zona con días default (lun-sab)
+ this.setZoneConfig(barrio, {
+ enabled: true,
+ days: ["lun", "mar", "mie", "jue", "vie", "sab"],
+ delivery_cost: 0
+ });
+ }
+ this.render();
+ });
+ });
+
+ // Zone day toggles
+ this.shadowRoot.querySelectorAll(".zone-day").forEach(dayBtn => {
+ dayBtn.addEventListener("click", () => {
+ const barrio = dayBtn.dataset.barrio;
+ const day = dayBtn.dataset.day;
+ const config = this.getZoneConfig(barrio);
+ if (!config) return;
+
+ const days = config.days || [];
+ const idx = days.indexOf(day);
+ if (idx >= 0) {
+ days.splice(idx, 1);
+ } else {
+ days.push(day);
+ }
+ config.days = days;
+ this.setZoneConfig(barrio, config);
+
+ // Update UI without full re-render
+ dayBtn.classList.toggle("active", days.includes(day));
+ });
+ });
+
+ // Zone cost inputs
+ this.shadowRoot.querySelectorAll(".zone-cost-input").forEach(input => {
+ input.addEventListener("change", () => {
+ const barrio = input.dataset.barrio;
+ const config = this.getZoneConfig(barrio);
+ if (!config) return;
+
+ config.delivery_cost = parseFloat(input.value) || 0;
+ this.setZoneConfig(barrio, config);
+ });
+ });
}
collectScheduleFromInputs() {
@@ -386,6 +611,9 @@ class SettingsCrud extends HTMLElement {
// Collect schedule from inputs
const schedule = this.collectScheduleFromInputs();
+ // Collect delivery zones (already in settings from event handlers)
+ const delivery_zones = this.settings.delivery_zones || {};
+
const data = {
store_name: this.shadowRoot.getElementById("storeName")?.value || "",
bot_name: this.shadowRoot.getElementById("botName")?.value || "",
@@ -395,6 +623,7 @@ class SettingsCrud extends HTMLElement {
pickup_enabled: this.settings.pickup_enabled,
delivery_min_order: parseFloat(this.shadowRoot.getElementById("deliveryMinOrder")?.value) || 0,
schedule,
+ delivery_zones,
};
// Update settings with form values
diff --git a/public/components/test-panel.js b/public/components/test-panel.js
index 93de0fd..ef732cd 100644
--- a/public/components/test-panel.js
+++ b/public/components/test-panel.js
@@ -46,7 +46,6 @@ class TestPanel extends HTMLElement {
this.selectedProducts = [];
this.testUser = null;
this.lastOrder = null;
- this.lastPaymentLink = null;
this.loading = false;
this.shadowRoot.innerHTML = `
@@ -66,11 +65,12 @@ class TestPanel extends HTMLElement {
height: 100%;
background: var(--bg);
color: var(--text);
- display: grid;
- grid-template-columns: 1fr 1fr;
+ display: flex;
+ flex-direction: column;
gap: 16px;
padding: 16px;
overflow: auto;
+ max-width: 600px;
}
.panel {
background: var(--panel);
@@ -227,50 +227,6 @@ class TestPanel extends HTMLElement {
-
-
-
2. Link de Pago (MercadoPago)
-
-
-
Monto
-
-
-
-
-
-
-
-
-
Link de pago
-
—
-
- Preference ID:
- —
-
-
-
-
-
3. Simular Pago Exitoso
-
-
-
- Simula el webhook de MercadoPago con status "approved".
- Esto actualiza la orden en WooCommerce a "processing".
-
-
-
-
-
-
-
Pago simulado
-
—
-
- Orden status:
- —
-
-
-
-
`;
}
@@ -279,8 +235,6 @@ class TestPanel extends HTMLElement {
this.shadowRoot.getElementById("btnGenerate").onclick = () => this.generateRandomOrder();
this.shadowRoot.getElementById("btnClear").onclick = () => this.clearAll();
this.shadowRoot.getElementById("btnCreateOrder").onclick = () => this.createOrder();
- this.shadowRoot.getElementById("btnPaymentLink").onclick = () => this.createPaymentLink();
- this.shadowRoot.getElementById("btnSimulateWebhook").onclick = () => this.simulateWebhook();
this.loadProducts();
}
@@ -379,16 +333,7 @@ class TestPanel extends HTMLElement {
updateButtonStates() {
const hasProducts = this.selectedProducts.length > 0;
- const hasOrder = this.lastOrder?.woo_order_id;
- const hasPaymentLink = this.lastPaymentLink?.init_point;
-
this.shadowRoot.getElementById("btnCreateOrder").disabled = !hasProducts;
- this.shadowRoot.getElementById("btnPaymentLink").disabled = !hasOrder;
- this.shadowRoot.getElementById("btnSimulateWebhook").disabled = !hasOrder;
-
- if (hasOrder) {
- this.shadowRoot.getElementById("inputAmount").value = this.lastOrder.total || "";
- }
}
async createOrder() {
@@ -433,101 +378,16 @@ class TestPanel extends HTMLElement {
}
}
- async createPaymentLink() {
- if (this.loading) return;
- if (!this.lastOrder?.woo_order_id) {
- modal.warn("Primero creá una orden");
- return;
- }
-
- const amount = parseFloat(this.shadowRoot.getElementById("inputAmount").value);
- if (!amount || amount <= 0) {
- modal.warn("Ingresá un monto válido");
- return;
- }
-
- this.loading = true;
- const btn = this.shadowRoot.getElementById("btnPaymentLink");
- btn.disabled = true;
- btn.textContent = "Generando...";
-
- try {
- const result = await api.createPaymentLink({
- woo_order_id: this.lastOrder.woo_order_id,
- amount,
- });
-
- if (result.ok) {
- this.lastPaymentLink = result;
- const linkEl = this.shadowRoot.getElementById("paymentLinkValue");
- linkEl.href = result.init_point || result.sandbox_init_point || "#";
- linkEl.textContent = result.init_point || result.sandbox_init_point || "—";
- this.shadowRoot.getElementById("preferenceIdValue").textContent = result.preference_id || "—";
- this.shadowRoot.getElementById("paymentResult").style.display = "block";
- } else {
- modal.error("Error: " + (result.error || "Error desconocido"));
- }
- } catch (e) {
- console.error("[test-panel] createPaymentLink error:", e);
- modal.error("Error generando link: " + e.message);
- } finally {
- this.loading = false;
- btn.textContent = "Generar Link de Pago";
- this.updateButtonStates();
- }
- }
-
- async simulateWebhook() {
- if (this.loading) return;
- if (!this.lastOrder?.woo_order_id) {
- modal.warn("Primero creá una orden");
- return;
- }
-
- this.loading = true;
- const btn = this.shadowRoot.getElementById("btnSimulateWebhook");
- btn.disabled = true;
- btn.textContent = "Simulando...";
-
- try {
- const amount = parseFloat(this.shadowRoot.getElementById("inputAmount").value) || this.lastOrder.total || 0;
-
- const result = await api.simulateMpWebhook({
- woo_order_id: this.lastOrder.woo_order_id,
- amount,
- });
-
- if (result.ok) {
- this.shadowRoot.getElementById("webhookStatusValue").textContent = `Payment ${result.payment_id} - ${result.status}`;
- this.shadowRoot.getElementById("webhookOrderStatusValue").textContent = result.order_status || "processing";
- this.shadowRoot.getElementById("webhookResult").style.display = "block";
- } else {
- modal.error("Error: " + (result.error || "Error desconocido"));
- }
- } catch (e) {
- console.error("[test-panel] simulateWebhook error:", e);
- modal.error("Error simulando webhook: " + e.message);
- } finally {
- this.loading = false;
- btn.textContent = "Simular Pago Exitoso";
- this.updateButtonStates();
- }
- }
-
clearAll() {
this.selectedProducts = [];
this.testUser = null;
this.lastOrder = null;
- this.lastPaymentLink = null;
this.renderProductList();
this.renderUserInfo();
this.updateButtonStates();
this.shadowRoot.getElementById("orderResult").style.display = "none";
- this.shadowRoot.getElementById("paymentResult").style.display = "none";
- this.shadowRoot.getElementById("webhookResult").style.display = "none";
- this.shadowRoot.getElementById("inputAmount").value = "";
}
}
diff --git a/public/lib/api.js b/public/lib/api.js
index 65d5af6..3ce6583 100644
--- a/public/lib/api.js
+++ b/public/lib/api.js
@@ -214,22 +214,6 @@ export const api = {
}).then(r => r.json());
},
- async createPaymentLink({ woo_order_id, amount }) {
- return fetch("/test/payment-link", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ woo_order_id, amount }),
- }).then(r => r.json());
- },
-
- async simulateMpWebhook({ woo_order_id, amount }) {
- return fetch("/test/simulate-webhook", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ woo_order_id, amount }),
- }).then(r => r.json());
- },
-
// --- Prompts CRUD ---
async prompts() {
return fetch("/prompts").then(r => r.json());
diff --git a/public/main.js b/public/main.js
index e9bceb2..7ea0c6f 100644
--- a/public/main.js
+++ b/public/main.js
@@ -70,7 +70,6 @@ function upsertConversation(chat_id, patch) {
* - call LLM (structured output)
* - product search (LIMITED) + resolve ids
* - create/update Woo order
- * - create MercadoPago link
* - save state
*/
async function processMessage({ chat_id, from, text }) {
diff --git a/src/app.js b/src/app.js
index c40682f..abf03e4 100644
--- a/src/app.js
+++ b/src/app.js
@@ -5,7 +5,6 @@ import { fileURLToPath } from "url";
import { createSimulatorRouter } from "./modules/1-intake/routes/simulator.js";
import { createEvolutionRouter } from "./modules/1-intake/routes/evolution.js";
-import { createMercadoPagoRouter } from "./modules/6-mercadopago/routes/mercadoPago.js";
import { createWooWebhooksRouter } from "./modules/2-identity/routes/wooWebhooks.js";
export function createApp({ tenantId }) {
@@ -23,7 +22,6 @@ export function createApp({ tenantId }) {
// --- Integraciones / UI ---
app.use(createSimulatorRouter({ tenantId }));
app.use(createEvolutionRouter());
- app.use("/payments/meli", createMercadoPagoRouter());
app.use(createWooWebhooksRouter());
// Home (UI)
diff --git a/src/modules/0-ui/controllers/testing.js b/src/modules/0-ui/controllers/testing.js
index ff0da29..62f80d5 100644
--- a/src/modules/0-ui/controllers/testing.js
+++ b/src/modules/0-ui/controllers/testing.js
@@ -2,8 +2,6 @@ import {
handleListOrders,
handleGetProductsWithStock,
handleCreateTestOrder,
- handleCreatePaymentLink,
- handleSimulateMpWebhook,
} from "../handlers/testing.js";
import { handleGetOrderStats } from "../handlers/stats.js";
@@ -59,47 +57,3 @@ export const makeCreateTestOrder = (tenantIdOrFn) => async (req, res) => {
}
};
-export const makeCreatePaymentLink = (tenantIdOrFn) => async (req, res) => {
- try {
- const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
- const { woo_order_id, amount } = req.body || {};
-
- if (!woo_order_id) {
- return res.status(400).json({ ok: false, error: "woo_order_id_required" });
- }
- if (!amount || Number(amount) <= 0) {
- return res.status(400).json({ ok: false, error: "amount_required" });
- }
-
- const result = await handleCreatePaymentLink({
- tenantId,
- wooOrderId: woo_order_id,
- amount: Number(amount)
- });
- res.json(result);
- } catch (err) {
- console.error("[testing] createPaymentLink error:", err);
- res.status(500).json({ ok: false, error: err.message || "internal_error" });
- }
-};
-
-export const makeSimulateMpWebhook = (tenantIdOrFn) => async (req, res) => {
- try {
- const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
- const { woo_order_id, amount } = req.body || {};
-
- if (!woo_order_id) {
- return res.status(400).json({ ok: false, error: "woo_order_id_required" });
- }
-
- const result = await handleSimulateMpWebhook({
- tenantId,
- wooOrderId: woo_order_id,
- amount: Number(amount) || 0
- });
- res.json(result);
- } catch (err) {
- console.error("[testing] simulateMpWebhook error:", err);
- res.status(500).json({ ok: false, error: err.message || "internal_error" });
- }
-};
diff --git a/src/modules/0-ui/db/settingsRepo.js b/src/modules/0-ui/db/settingsRepo.js
index c75e938..1711ed5 100644
--- a/src/modules/0-ui/db/settingsRepo.js
+++ b/src/modules/0-ui/db/settingsRepo.js
@@ -20,6 +20,7 @@ export async function getSettings({ tenantId }) {
pickup_hours_start::text as pickup_hours_start,
pickup_hours_end::text as pickup_hours_end,
schedule,
+ delivery_zones,
created_at, updated_at
FROM tenant_settings
WHERE tenant_id = $1
@@ -48,6 +49,7 @@ export async function upsertSettings({ tenantId, settings }) {
pickup_hours_start,
pickup_hours_end,
schedule,
+ delivery_zones,
} = settings;
const sql = `
@@ -55,9 +57,9 @@ export async function upsertSettings({ tenantId, settings }) {
tenant_id, store_name, bot_name, store_address, store_phone,
delivery_enabled, delivery_days, delivery_hours_start, delivery_hours_end, delivery_min_order,
pickup_enabled, pickup_days, pickup_hours_start, pickup_hours_end,
- schedule
+ schedule, delivery_zones
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (tenant_id) DO UPDATE SET
store_name = COALESCE(EXCLUDED.store_name, tenant_settings.store_name),
bot_name = COALESCE(EXCLUDED.bot_name, tenant_settings.bot_name),
@@ -73,6 +75,7 @@ export async function upsertSettings({ tenantId, settings }) {
pickup_hours_start = COALESCE(EXCLUDED.pickup_hours_start, tenant_settings.pickup_hours_start),
pickup_hours_end = COALESCE(EXCLUDED.pickup_hours_end, tenant_settings.pickup_hours_end),
schedule = COALESCE(EXCLUDED.schedule, tenant_settings.schedule),
+ delivery_zones = COALESCE(EXCLUDED.delivery_zones, tenant_settings.delivery_zones),
updated_at = NOW()
RETURNING
id, tenant_id,
@@ -85,6 +88,7 @@ export async function upsertSettings({ tenantId, settings }) {
pickup_hours_start::text as pickup_hours_start,
pickup_hours_end::text as pickup_hours_end,
schedule,
+ delivery_zones,
created_at, updated_at
`;
@@ -104,6 +108,7 @@ export async function upsertSettings({ tenantId, settings }) {
pickup_hours_start || null,
pickup_hours_end || null,
schedule ? JSON.stringify(schedule) : null,
+ delivery_zones ? JSON.stringify(delivery_zones) : null,
];
const { rows } = await pool.query(sql, params);
diff --git a/src/modules/0-ui/handlers/settings.js b/src/modules/0-ui/handlers/settings.js
index c31e295..86da45b 100644
--- a/src/modules/0-ui/handlers/settings.js
+++ b/src/modules/0-ui/handlers/settings.js
@@ -42,6 +42,7 @@ export async function handleGetSettings({ tenantId }) {
pickup_hours_start: "08:00",
pickup_hours_end: "20:00",
schedule: createDefaultSchedule(),
+ delivery_zones: {},
is_default: true,
};
}
@@ -60,6 +61,7 @@ export async function handleGetSettings({ tenantId }) {
pickup_hours_start: settings.pickup_hours_start?.slice(0, 5) || "08:00",
pickup_hours_end: settings.pickup_hours_end?.slice(0, 5) || "20:00",
schedule,
+ delivery_zones: settings.delivery_zones || {},
is_default: false,
};
}
@@ -103,7 +105,8 @@ function buildScheduleFromLegacy(settings) {
function validateSchedule(schedule) {
if (!schedule || typeof schedule !== "object") return;
- const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
+ // Acepta HH:MM o HH:MM:SS (la BD puede devolver con segundos)
+ const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
for (const type of ["delivery", "pickup"]) {
const typeSchedule = schedule[type];
@@ -240,6 +243,7 @@ export async function handleSaveSettings({ tenantId, settings }) {
delivery_hours_end: result.delivery_hours_end?.slice(0, 5),
pickup_hours_start: result.pickup_hours_start?.slice(0, 5),
pickup_hours_end: result.pickup_hours_end?.slice(0, 5),
+ delivery_zones: result.delivery_zones || {},
},
message: "Configuración guardada correctamente",
};
diff --git a/src/modules/0-ui/handlers/testing.js b/src/modules/0-ui/handlers/testing.js
index 8225caa..59c76ea 100644
--- a/src/modules/0-ui/handlers/testing.js
+++ b/src/modules/0-ui/handlers/testing.js
@@ -1,5 +1,4 @@
import { createOrder, syncOrdersIncremental } from "../../4-woo-orders/wooOrders.js";
-import { createPreference, reconcilePayment } from "../../6-mercadopago/mercadoPago.js";
import { listProducts } from "../db/repo.js";
import * as ordersRepo from "../../4-woo-orders/ordersRepo.js";
@@ -73,74 +72,3 @@ export async function handleCreateTestOrder({ tenantId, basket, address, wa_chat
};
}
-/**
- * Crea un link de pago de MercadoPago
- */
-export async function handleCreatePaymentLink({ tenantId, wooOrderId, amount }) {
- if (!wooOrderId) {
- throw new Error("missing_woo_order_id");
- }
- if (!amount || Number(amount) <= 0) {
- throw new Error("invalid_amount");
- }
-
- const pref = await createPreference({
- tenantId,
- wooOrderId,
- amount: Number(amount),
- });
-
- return {
- ok: true,
- preference_id: pref?.preference_id || null,
- init_point: pref?.init_point || null,
- sandbox_init_point: pref?.sandbox_init_point || null,
- };
-}
-
-/**
- * Simula un webhook de MercadoPago con pago exitoso
- * No pasa por el endpoint real (requiere firma HMAC)
- * Crea un payment mock y llama a reconcilePayment directamente
- */
-export async function handleSimulateMpWebhook({ tenantId, wooOrderId, amount }) {
- if (!wooOrderId) {
- throw new Error("missing_woo_order_id");
- }
-
- // Crear payment mock con status approved
- const mockPaymentId = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
- const mockPayment = {
- id: mockPaymentId,
- status: "approved",
- status_detail: "accredited",
- external_reference: `${tenantId}|${wooOrderId}`,
- transaction_amount: Number(amount) || 0,
- currency_id: "ARS",
- date_approved: new Date().toISOString(),
- date_created: new Date().toISOString(),
- payment_method_id: "test",
- payment_type_id: "credit_card",
- payer: {
- email: "test@test.com",
- },
- order: {
- id: `pref-test-${wooOrderId}`,
- },
- };
-
- // Reconciliar el pago (actualiza mp_payments y cambia status de orden a processing)
- const result = await reconcilePayment({
- tenantId,
- payment: mockPayment,
- });
-
- return {
- ok: true,
- payment_id: mockPaymentId,
- woo_order_id: result?.woo_order_id || wooOrderId,
- status: "approved",
- order_status: "processing",
- reconciled: result?.payment || null,
- };
-}
diff --git a/src/modules/1-intake/routes/simulator.js b/src/modules/1-intake/routes/simulator.js
index 8d6121d..799e125 100644
--- a/src/modules/1-intake/routes/simulator.js
+++ b/src/modules/1-intake/routes/simulator.js
@@ -14,7 +14,7 @@ import { makeListPrompts, makeGetPrompt, makeSavePrompt, makeRollbackPrompt, mak
import { makeListPendingTakeovers, makeListAllTakeovers, makeGetTakeover, makeRespondToTakeover, makeCancelTakeover, makeCheckPendingTakeover } from "../../0-ui/controllers/takeovers.js";
import { makeGetSettings, makeSaveSettings } from "../../0-ui/controllers/settings.js";
import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "../../0-ui/controllers/admin.js";
-import { makeListOrders, makeGetOrderStats, makeGetProductsWithStock, makeCreateTestOrder, makeCreatePaymentLink, makeSimulateMpWebhook } from "../../0-ui/controllers/testing.js";
+import { makeListOrders, makeGetOrderStats, makeGetProductsWithStock, makeCreateTestOrder } from "../../0-ui/controllers/testing.js";
function nowIso() {
return new Date().toISOString();
@@ -114,8 +114,6 @@ export function createSimulatorRouter({ tenantId }) {
// --- Testing routes ---
router.get("/test/products-with-stock", makeGetProductsWithStock(getTenantId));
router.post("/test/order", makeCreateTestOrder(getTenantId));
- router.post("/test/payment-link", makeCreatePaymentLink(getTenantId));
- router.post("/test/simulate-webhook", makeSimulateMpWebhook(getTenantId));
return router;
}
diff --git a/src/modules/2-identity/db/repo.js b/src/modules/2-identity/db/repo.js
index c9c9d73..2e704ed 100644
--- a/src/modules/2-identity/db/repo.js
+++ b/src/modules/2-identity/db/repo.js
@@ -733,51 +733,4 @@ export async function upsertProductEmbedding({
return rows[0] || null;
}
-export async function upsertMpPayment({
- tenant_id,
- woo_order_id = null,
- preference_id = null,
- payment_id = null,
- status = null,
- paid_at = null,
- raw = {},
-}) {
- if (!payment_id) throw new Error("payment_id_required");
- const sql = `
- insert into mp_payments
- (tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, created_at, updated_at)
- values
- ($1, $2, $3, $4, $5, $6::timestamptz, $7::jsonb, now(), now())
- on conflict (tenant_id, payment_id)
- do update set
- woo_order_id = excluded.woo_order_id,
- preference_id = excluded.preference_id,
- status = excluded.status,
- paid_at = excluded.paid_at,
- raw = excluded.raw,
- updated_at = now()
- returning tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, updated_at
- `;
- const { rows } = await pool.query(sql, [
- tenant_id,
- woo_order_id,
- preference_id,
- payment_id,
- status,
- paid_at,
- JSON.stringify(raw ?? {}),
- ]);
- return rows[0] || null;
-}
-
-export async function getMpPaymentById({ tenant_id, payment_id }) {
- const sql = `
- select tenant_id, woo_order_id, preference_id, payment_id, status, paid_at, raw, updated_at
- from mp_payments
- where tenant_id=$1 and payment_id=$2
- limit 1
- `;
- const { rows } = await pool.query(sql, [tenant_id, payment_id]);
- return rows[0] || null;
-}
diff --git a/src/modules/2-identity/services/pipeline.js b/src/modules/2-identity/services/pipeline.js
index 50cd5c3..5531d12 100644
--- a/src/modules/2-identity/services/pipeline.js
+++ b/src/modules/2-identity/services/pipeline.js
@@ -17,7 +17,6 @@ import { debug as dbg } from "../../shared/debug.js";
import { runTurnV3 } from "../../3-turn-engine/turnEngineV3.js";
import { safeNextState } from "../../3-turn-engine/fsm.js";
import { createOrder, updateOrder } from "../../4-woo-orders/wooOrders.js";
-import { createPreference } from "../../6-mercadopago/mercadoPago.js";
import { handleCreateTakeover } from "../../0-ui/handlers/takeovers.js";
import { sendTextMessage, isEvolutionEnabled } from "../../1-intake/services/evolutionSender.js";
@@ -313,25 +312,6 @@ const runStatus = llmMeta?.error ? "warn" : "ok";
pending_query: act.payload?.pending_query,
});
}
- } else if (act.type === "send_payment_link") {
- const total = Number(actionPatch?.order_total || reducedContext?.order_total || 0) || null;
- if (!total || total <= 0) {
- throw new Error("order_total_missing");
- }
- const pref = await createPreference({
- tenantId,
- wooOrderId: actionPatch.woo_order_id || reducedContext?.woo_order_id || prev?.last_order_id,
- amount: total || 0,
- });
- actionPatch.payment_link = pref?.init_point || null;
- actionPatch.mp = {
- preference_id: pref?.preference_id || null,
- init_point: pref?.init_point || null,
- };
- newTools.push({ type: "send_payment_link", ok: true, preference_id: pref?.preference_id || null });
- if (pref?.init_point) {
- plan.reply = `Listo, acá tenés el link de pago: ${pref.init_point}\nAvisame cuando esté listo.`;
- }
}
} catch (e) {
newTools.push({ type: act.type, ok: false, error: String(e?.message || e) });
diff --git a/src/modules/3-turn-engine/nlu/index.js b/src/modules/3-turn-engine/nlu/index.js
index 3a3bc95..a5af1d4 100644
--- a/src/modules/3-turn-engine/nlu/index.js
+++ b/src/modules/3-turn-engine/nlu/index.js
@@ -162,6 +162,12 @@ function shouldSkipRouter(text, state, quickDomain) {
return true;
}
+ // En estado SHIPPING, si quickDomain ya detectó "shipping" (dirección), confiar en eso
+ // Esto evita que el router LLM clasifique direcciones como productos
+ if (state === "SHIPPING" && quickDomain === "shipping") {
+ return true;
+ }
+
return false;
}
diff --git a/src/modules/3-turn-engine/stateHandlers/payment.js b/src/modules/3-turn-engine/stateHandlers/payment.js
index 7bd7060..be7a667 100644
--- a/src/modules/3-turn-engine/stateHandlers/payment.js
+++ b/src/modules/3-turn-engine/stateHandlers/payment.js
@@ -31,10 +31,6 @@ export async function handlePaymentState({ tenantId, text, nlu, order, audit })
currentOrder = { ...currentOrder, payment_type: paymentMethod };
actions.push({ type: "create_order", payload: { payment: paymentMethod } });
- if (paymentMethod === "link") {
- actions.push({ type: "send_payment_link", payload: {} });
- }
-
const { next_state } = safeNextState(ConversationState.PAYMENT, currentOrder, { payment_selected: true });
const paymentLabel = paymentMethod === "cash" ? "efectivo" : "link de pago";
@@ -43,7 +39,7 @@ export async function handlePaymentState({ tenantId, text, nlu, order, audit })
: "Retiro en sucursal.";
const paymentInfo = paymentMethod === "link"
- ? "Te paso el link de pago en un momento."
+ ? "Te contactamos para coordinar el pago."
: "Pagás en " + (currentOrder.is_delivery ? "la entrega" : "sucursal") + ".";
return {
diff --git a/src/modules/6-mercadopago/controllers/mercadoPago.js b/src/modules/6-mercadopago/controllers/mercadoPago.js
deleted file mode 100644
index eb2ba23..0000000
--- a/src/modules/6-mercadopago/controllers/mercadoPago.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { fetchPayment, reconcilePayment, verifyWebhookSignature } from "../mercadoPago.js";
-
-export function makeMercadoPagoWebhook() {
- return async function handleMercadoPagoWebhook(req, res) {
- try {
- const signature = verifyWebhookSignature({ headers: req.headers, query: req.query || {} });
- if (!signature.ok) {
- return res.status(401).json({ ok: false, error: "invalid_signature", reason: signature.reason });
- }
-
- const paymentId =
- req?.query?.["data.id"] ||
- req?.query?.data?.id ||
- req?.body?.data?.id ||
- null;
-
- if (!paymentId) {
- return res.status(400).json({ ok: false, error: "missing_payment_id" });
- }
-
- const payment = await fetchPayment({ paymentId });
- const reconciled = await reconcilePayment({ payment });
-
- return res.status(200).json({
- ok: true,
- payment_id: payment?.id || null,
- status: payment?.status || null,
- woo_order_id: reconciled?.woo_order_id || null,
- });
- } catch (e) {
- return res.status(500).json({ ok: false, error: String(e?.message || e) });
- }
- };
-}
-
-export function makeMercadoPagoReturn() {
- return function handleMercadoPagoReturn(req, res) {
- const status = req.query?.status || "unknown";
- res.status(200).send(`OK - ${status}`);
- };
-}
-
diff --git a/src/modules/6-mercadopago/mercadoPago.js b/src/modules/6-mercadopago/mercadoPago.js
deleted file mode 100644
index d90028c..0000000
--- a/src/modules/6-mercadopago/mercadoPago.js
+++ /dev/null
@@ -1,178 +0,0 @@
-import crypto from "crypto";
-import { upsertMpPayment } from "../2-identity/db/repo.js";
-import { updateOrderStatus } from "../4-woo-orders/wooOrders.js";
-
-function getAccessToken() {
- return process.env.MP_ACCESS_TOKEN || null;
-}
-
-function getWebhookSecret() {
- return process.env.MP_WEBHOOK_SECRET || null;
-}
-
-function normalizeBaseUrl(base) {
- if (!base) return null;
- return base.endsWith("/") ? base : `${base}/`;
-}
-
-function getBaseUrl() {
- return normalizeBaseUrl(process.env.MP_BASE_URL || process.env.MP_WEBHOOK_BASE_URL || null);
-}
-
-async function fetchMp({ url, method = "GET", body = null }) {
- const token = getAccessToken();
- if (!token) throw new Error("MP_ACCESS_TOKEN is not set");
- const res = await fetch(url, {
- method,
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: body ? JSON.stringify(body) : undefined,
- });
- const text = await res.text();
- let parsed;
- try {
- parsed = text ? JSON.parse(text) : null;
- } catch {
- parsed = text;
- }
- if (!res.ok) {
- const err = new Error(`MP HTTP ${res.status}`);
- err.status = res.status;
- err.body = parsed;
- throw err;
- }
- return parsed;
-}
-
-export async function createPreference({
- tenantId,
- wooOrderId,
- amount,
- payer = null,
- items = null,
- baseUrl = null,
-}) {
- const root = normalizeBaseUrl(baseUrl || getBaseUrl());
- if (!root) throw new Error("MP_BASE_URL is not set");
- const notificationUrl = `${root}webhook/mercadopago`;
- const backUrls = {
- success: `${root}return?status=success`,
- failure: `${root}return?status=failure`,
- pending: `${root}return?status=pending`,
- };
- const statementDescriptor = process.env.MP_STATEMENT_DESCRIPTOR || "Whatsapp Store";
- const externalReference = `${tenantId}|${wooOrderId}`;
- const unitPrice = Number(amount);
- if (!Number.isFinite(unitPrice)) throw new Error("invalid_amount");
-
- const payload = {
- auto_return: "approved",
- back_urls: backUrls,
- statement_descriptor: statementDescriptor,
- binary_mode: false,
- external_reference: externalReference,
- items: Array.isArray(items) && items.length
- ? items
- : [
- {
- id: String(wooOrderId || "order"),
- title: "Productos x whatsapp",
- quantity: 1,
- currency_id: "ARS",
- unit_price: unitPrice,
- },
- ],
- notification_url: notificationUrl,
- ...(payer ? { payer } : {}),
- };
-
- const data = await fetchMp({
- url: "https://api.mercadopago.com/checkout/preferences",
- method: "POST",
- body: payload,
- });
-
- return {
- preference_id: data?.id || null,
- init_point: data?.init_point || null,
- sandbox_init_point: data?.sandbox_init_point || null,
- raw: data,
- };
-}
-
-function parseSignatureHeader(header) {
- const h = String(header || "");
- const parts = h.split(",");
- let ts = null;
- let v1 = null;
- for (const p of parts) {
- const [k, v] = p.split("=");
- if (!k || !v) continue;
- const key = k.trim();
- const val = v.trim();
- if (key === "ts") ts = val;
- if (key === "v1") v1 = val;
- }
- return { ts, v1 };
-}
-
-export function verifyWebhookSignature({ headers = {}, query = {} }) {
- const secret = getWebhookSecret();
- if (!secret) return { ok: false, reason: "MP_WEBHOOK_SECRET is not set" };
- const xSignature = headers["x-signature"] || headers["X-Signature"] || headers["x-signature"];
- const xRequestId = headers["x-request-id"] || headers["X-Request-Id"] || headers["x-request-id"];
- const { ts, v1 } = parseSignatureHeader(xSignature);
- const dataId = query["data.id"] || query?.data?.id || null;
- if (!xRequestId || !ts || !v1 || !dataId) {
- return { ok: false, reason: "missing_signature_fields" };
- }
- const manifest = `id:${String(dataId).toLowerCase()};request-id:${xRequestId};ts:${ts};`;
- const hmac = crypto.createHmac("sha256", secret);
- hmac.update(manifest);
- const hash = hmac.digest("hex");
- const ok = crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(v1));
- return ok ? { ok: true } : { ok: false, reason: "invalid_signature" };
-}
-
-export async function fetchPayment({ paymentId }) {
- if (!paymentId) throw new Error("missing_payment_id");
- return await fetchMp({
- url: `https://api.mercadopago.com/v1/payments/${encodeURIComponent(paymentId)}`,
- method: "GET",
- });
-}
-
-export function parseExternalReference(externalReference) {
- if (!externalReference) return { tenantId: null, wooOrderId: null };
- const parts = String(externalReference).split("|").filter(Boolean);
- if (parts.length >= 2) {
- return { tenantId: parts[0], wooOrderId: Number(parts[1]) || null };
- }
- return { tenantId: null, wooOrderId: Number(externalReference) || null };
-}
-
-export async function reconcilePayment({ tenantId, payment }) {
- const status = payment?.status || null;
- const paidAt = payment?.date_approved || payment?.date_created || null;
- const { tenantId: refTenantId, wooOrderId } = parseExternalReference(payment?.external_reference);
- const resolvedTenantId = tenantId || refTenantId;
- if (!resolvedTenantId) throw new Error("tenant_id_missing_from_payment");
- const saved = await upsertMpPayment({
- tenant_id: resolvedTenantId,
- woo_order_id: wooOrderId,
- preference_id: payment?.order?.id || payment?.preference_id || null,
- payment_id: String(payment?.id || ""),
- status,
- paid_at: paidAt,
- raw: payment,
- });
-
- if (status === "approved" && wooOrderId) {
- await updateOrderStatus({ tenantId: resolvedTenantId, wooOrderId, status: "processing" });
- }
-
- return { payment: saved, woo_order_id: wooOrderId, tenant_id: resolvedTenantId };
-}
-
diff --git a/src/modules/6-mercadopago/routes/mercadoPago.js b/src/modules/6-mercadopago/routes/mercadoPago.js
deleted file mode 100644
index ad1ebe0..0000000
--- a/src/modules/6-mercadopago/routes/mercadoPago.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import express from "express";
-import { makeMercadoPagoReturn, makeMercadoPagoWebhook } from "../controllers/mercadoPago.js";
-
-export function createMercadoPagoRouter() {
- const router = express.Router();
- router.post("/webhook/mercadopago", makeMercadoPagoWebhook());
- router.get("/return", makeMercadoPagoReturn());
- return router;
-}
-