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:
@@ -48,6 +48,50 @@ function formatDaySlot(slot) {
|
||||
/**
|
||||
* Resumen de zonas de delivery: lista de barrios o "Consultar zonas".
|
||||
*/
|
||||
/**
|
||||
* Lista plana de barrios (lowercase) habilitados para delivery.
|
||||
*/
|
||||
function getDeliveryZoneNames(deliveryZones) {
|
||||
if (!deliveryZones || typeof deliveryZones !== "object") return [];
|
||||
const names = [];
|
||||
if (Array.isArray(deliveryZones.zones)) {
|
||||
for (const z of deliveryZones.zones) if (z?.name) names.push(z.name);
|
||||
} else if (deliveryZones.caba?.barrios) {
|
||||
if (Array.isArray(deliveryZones.caba.barrios)) names.push(...deliveryZones.caba.barrios);
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(deliveryZones)) {
|
||||
if (v === true) names.push(k);
|
||||
else if (v?.name) names.push(v.name);
|
||||
}
|
||||
}
|
||||
return names.map((n) => String(n).toLowerCase().trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un texto de dirección menciona un barrio en zona.
|
||||
* - Sin zonas configuradas: retorna `{ inZone: true, reason: "no_zones_configured" }`
|
||||
* (no bloqueamos hasta que el comercio cargue las zonas).
|
||||
* - Con zonas: matching simple por inclusion del barrio en el texto (lowercase, sin tildes).
|
||||
*/
|
||||
export function checkAddressInZone({ address, storeConfig }) {
|
||||
const zones = getDeliveryZoneNames(storeConfig?.delivery_zones);
|
||||
if (!zones.length) {
|
||||
return { inZone: true, reason: "no_zones_configured", zones: [] };
|
||||
}
|
||||
const norm = String(address || "")
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[̀-ͯ]/g, "")
|
||||
.trim();
|
||||
if (!norm) return { inZone: false, reason: "empty_address", zones };
|
||||
const match = zones.find((z) => {
|
||||
const zNorm = z.normalize("NFD").replace(/[̀-ͯ]/g, "");
|
||||
return norm.includes(zNorm);
|
||||
});
|
||||
if (match) return { inZone: true, reason: "matched", matched: match, zones };
|
||||
return { inZone: false, reason: "not_in_zone", zones };
|
||||
}
|
||||
|
||||
function summarizeDeliveryZones(deliveryZones) {
|
||||
if (!deliveryZones || typeof deliveryZones !== "object") return "";
|
||||
const names = [];
|
||||
|
||||
Reference in New Issue
Block a user