Mejoras: idempotency webhook, métricas rewriter, recommend en XState, seeds
7 frentes en una pasada:
1. Idempotency en pipeline.processMessage: si el message_id ya existía
(Evolution suele reentregar webhooks), skip todo el turn y devuelve
{ duplicate: true }. Antes el ON CONFLICT DO NOTHING evitaba el insert
pero igual procesaba NLU + side effects.
2. Limpieza de payment residual de admin/UI:
- ordersRepo.getMonthlyStats / getTotals: out las queries cash/card
- home-dashboard: out el donut "Efectivo vs Tarjeta"
- orders-crud: out columna "Pago" + sección de detalle de pago
- conversation-inspector: out 💵💳✅ del resumen
- takeovers: out payment_link en run, out payment del summary
- public/main.js: out la invariant no_checkout_without_payment_link
- prompts-crud: out la entry "payment" del PROMPT_LABELS
- wooOrders.parseOrder: out lectura de payment_method/is_paid (estado
del pago lo gestiona el comercio offline, fuera del bot)
- ordersRepo: out is_cash/is_paid del row mapping
3. Seed migration 20260501130000_seed_piaf_settings_and_replies:
- schedule realista para piaf (delivery/pickup días+horas)
- delivery_zones con barrios CABA reales (Palermo, Belgrano, etc)
- 41 reply_templates con 17 keys + variantes (todo lo de DEFAULTS)
Permite editar respuestas sin redeploy y desbloquea {{store_hours_today}},
{{delivery_zones_summary}} reales en los templates.
4. Address validation en shipping: nuevo checkAddressInZone() en
storeContext.js. Cuando el usuario da dirección, se valida contra
zonas configuradas. Si está fuera, renderiza shipping.address_out_of_zone
con sugerencia de zonas alternativas. Sin zonas configuradas → accept-by-default.
5. Métricas rewriter: getRewriterMetrics() expuesto, contadores
ok/fallback/timeouts + avg_ms + fallback_rate. Endpoint nuevo
/api/metrics/rewriter.
6. Shadow XState → audit_log: el shadow mode pasa de console.log a
insertAuditLog con entity_type='xstate_shadow'. Permite review post-mortem.
7. Recommend portado a XState: nuevo recommendActor (fromPromise) wrappea
handleRecommend; sub-state cart.recommending invoca el actor; ingestRecommendResult
absorbe { plan, decision } en context. RECOMMEND event funciona desde idle
y desde cart con USE_XSTATE=1.
8. tenantId opcional en renderReply/loadReplyVariants — defaultea a
getTenantId() del módulo shared/tenant.js. Backward-compat: callers
pueden seguir pasando tenantId o omitirlo.
E2E tests nuevos en machine/e2e.test.js: golden flow pickup, golden flow
delivery con address-in-zone, snapshot rehydrate full flow, universal
cart-on-add desde shipping. 192/192 tests pasando (188 previos + 4 E2E).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -206,13 +206,10 @@ class ConversationInspector extends HTMLElement {
|
||||
parts.push(`[${activePending.length} pendiente(s)]`);
|
||||
}
|
||||
|
||||
// Checkout info
|
||||
// Checkout info (sólo shipping — el bot no maneja pagos)
|
||||
const checkoutInfo = [];
|
||||
if (order?.is_delivery === true) checkoutInfo.push("🚚");
|
||||
if (order?.is_delivery === false) checkoutInfo.push("🏪");
|
||||
if (order?.payment_type === "cash") checkoutInfo.push("💵");
|
||||
if (order?.payment_type === "link") checkoutInfo.push("💳");
|
||||
if (order?.is_paid) checkoutInfo.push("✅");
|
||||
if (checkoutInfo.length) parts.push(checkoutInfo.join(""));
|
||||
|
||||
return parts.length ? parts.join(" ") : "—";
|
||||
@@ -232,7 +229,6 @@ class ConversationInspector extends HTMLElement {
|
||||
"ensure_woo_customer": "woo customer",
|
||||
"create_order": "create order",
|
||||
"update_order": "update order",
|
||||
"send_payment_link": "payment link",
|
||||
"request_human_takeover": "human takeover",
|
||||
"add_to_cart": "add to cart",
|
||||
"human_response_sent": "human response",
|
||||
|
||||
@@ -164,12 +164,6 @@ class HomeDashboard extends HTMLElement {
|
||||
<canvas id="shipping-donut"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">Efectivo vs Tarjeta</div>
|
||||
<div class="chart-container short">
|
||||
<canvas id="payment-donut"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-card full-width">
|
||||
<div class="chart-title">Top Productos por Facturación</div>
|
||||
@@ -455,22 +449,6 @@ class HomeDashboard extends HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Payment donut
|
||||
const paymentCtx = this.shadowRoot.getElementById("payment-donut");
|
||||
if (paymentCtx) {
|
||||
if (this.charts.paymentDonut) this.charts.paymentDonut.destroy();
|
||||
this.charts.paymentDonut = new Chart(paymentCtx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Efectivo", "Tarjeta"],
|
||||
datasets: [{
|
||||
data: [totals.by_payment?.cash || 0, totals.by_payment?.card || 0],
|
||||
backgroundColor: ["#10B981", "#EC4899"],
|
||||
}],
|
||||
},
|
||||
options: this.getDonutOptions(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDonutOptions() {
|
||||
|
||||
@@ -431,7 +431,6 @@ class OrdersCrud extends HTMLElement {
|
||||
<th>Tipo</th>
|
||||
<th>Estado</th>
|
||||
<th>Envío</th>
|
||||
<th>Pago</th>
|
||||
<th>Cliente</th>
|
||||
<th>Total</th>
|
||||
<th>Fecha</th>
|
||||
@@ -453,12 +452,6 @@ class OrdersCrud extends HTMLElement {
|
||||
</td>
|
||||
<td><span class="status-badge" style="background:${statusColor(order.status)}">${statusLabel(order.status)}</span></td>
|
||||
<td><span class="badge" style="background:${order.is_delivery ? '#3b82f6' : '#8b5cf6'};color:#fff">${order.is_delivery ? 'DEL' : 'RET'}</span></td>
|
||||
<td>
|
||||
<div class="badges">
|
||||
<span class="badge" style="background:${order.is_cash ? '#f59e0b' : '#1f6feb'};color:${order.is_cash ? '#000' : '#fff'}">${order.is_cash ? '$' : 'MP'}</span>
|
||||
<span class="badge" style="background:${order.is_paid ? '#22c55e' : '#ef4444'};color:#fff">${order.is_paid ? '✓' : '✗'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="customer-name" title="${customerName}">${customerName}</td>
|
||||
<td class="total">$${Number(order.total || 0).toLocaleString("es-AR")}</td>
|
||||
<td>${formatDate(order.date_created)}</td>
|
||||
@@ -579,28 +572,6 @@ class OrdersCrud extends HTMLElement {
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-title">Pago</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Método</span>
|
||||
<span class="detail-value">
|
||||
<span class="badge" style="background:${order.is_cash ? '#f59e0b' : '#1f6feb'};color:${order.is_cash ? '#000' : '#fff'};padding:3px 8px;border-radius:4px;font-size:10px;">
|
||||
${order.is_cash ? 'EFECTIVO' : 'LINK'}
|
||||
</span>
|
||||
${order.payment_method_title ? `<span style="margin-left:8px;color:var(--muted);font-size:11px;">${order.payment_method_title}</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Estado</span>
|
||||
<span class="detail-value">
|
||||
<span class="badge" style="background:${order.is_paid ? '#22c55e' : '#ef4444'};color:#fff;padding:3px 8px;border-radius:4px;font-size:10px;">
|
||||
${order.is_paid ? 'PAGADO' : 'PENDIENTE'}
|
||||
</span>
|
||||
${order.date_paid ? `<span style="margin-left:8px;color:var(--muted);font-size:11px;">${formatDate(order.date_paid)}</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="detail-title">Cliente</div>
|
||||
<div class="detail-row">
|
||||
|
||||
@@ -7,7 +7,6 @@ const PROMPT_LABELS = {
|
||||
greeting: "Saludos",
|
||||
orders: "Pedidos",
|
||||
shipping: "Envio/Retiro",
|
||||
payment: "Pago",
|
||||
browse: "Consultas de catalogo",
|
||||
};
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ async function processMessage({ chat_id, from, text }) {
|
||||
ok: true,
|
||||
checks: [
|
||||
{ name: "required_keys_present", ok: true },
|
||||
{ name: "no_checkout_without_payment_link", ok: true },
|
||||
{ name: "no_order_action_without_items", ok: true },
|
||||
],
|
||||
};
|
||||
@@ -110,7 +109,6 @@ async function processMessage({ chat_id, from, text }) {
|
||||
invariants,
|
||||
final_reply: plan.reply,
|
||||
order_id: null,
|
||||
payment_link: null,
|
||||
latency_ms: Date.now() - started_at,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user