/** * Store Context — helpers para inyectar info de la tienda en el agente. * * Schema canónico de delivery_zones (JSONB en tenant_settings): * { * zones: [ * { * id: "centro", * name: "Centro / Microcentro", * polygon: { type: "Polygon", coordinates: [[[lng,lat], ...]] }, * delivery_cost: 1500, * delivery_days: ["lun","mar","mie","jue","vie","sab"], * delivery_hours: { start: "10:00", end: "20:00" }, * min_order_amount: 0, * enabled: true * } * ], * default_center: [lng, lat] // opcional, para el editor * } */ import { findZoneForPoint } from "./lib/geo.js"; const DAY_KEYS_SHORT = ["lun", "mar", "mie", "jue", "vie", "sab", "dom"]; const DAY_NAMES = { lun: "Lunes", mar: "Martes", mie: "Miércoles", jue: "Jueves", vie: "Viernes", sab: "Sábado", dom: "Domingo", }; function todayShortKey() { // Date.getDay(): 0=Dom..6=Sab → mapeamos a lun..dom (0=lun) return DAY_KEYS_SHORT[(new Date().getDay() + 6) % 7]; } function pickDaySlot(scheduleObj, dayKey) { if (!scheduleObj || typeof scheduleObj !== "object") return null; return scheduleObj[dayKey] || null; } function formatDaySlot(slot) { if (!slot) return null; if (slot.enabled === false) return null; const start = (slot.start || "").slice(0, 5); const end = (slot.end || "").slice(0, 5); if (!start || !end) return null; return `${start} a ${end}`; } function getZones(deliveryZones) { if (!deliveryZones || typeof deliveryZones !== "object") return []; if (!Array.isArray(deliveryZones.zones)) return []; return deliveryZones.zones.filter((z) => z && z.enabled !== false); } /** * Devuelve los nombres de zonas habilitadas (sin polygon, para UI/log). */ export function getDeliveryZoneNames(deliveryZones) { return getZones(deliveryZones).map((z) => String(z.name || z.id || "").trim()).filter(Boolean); } /** * Verifica si una ubicación (lat/lng) cae dentro de alguna zona habilitada. * * @param {Object} args * @param {{lat:number, lng:number}} [args.location] * @param {Object} args.storeConfig * @returns {{inZone:boolean, reason:string, matched_zone?:Object, zones:Array}} */ export function checkAddressInZone({ location, storeConfig }) { const allZones = getZones(storeConfig?.delivery_zones); if (!allZones.length) { return { inZone: true, reason: "no_zones_configured", zones: [] }; } if (!location || typeof location.lat !== "number" || typeof location.lng !== "number") { return { inZone: false, reason: "need_location", zones: allZones.map(zoneSummary), }; } const matched = findZoneForPoint(location.lng, location.lat, allZones); if (matched) { return { inZone: true, reason: "matched", matched_zone: zoneSummary(matched), zones: allZones.map(zoneSummary) }; } return { inZone: false, reason: "out_of_zones", zones: allZones.map(zoneSummary) }; } function zoneSummary(z) { return { id: z.id, name: z.name, delivery_cost: z.delivery_cost ?? null, delivery_days: Array.isArray(z.delivery_days) ? z.delivery_days : [], delivery_hours: z.delivery_hours || null, min_order_amount: z.min_order_amount ?? 0, }; } /** * Formato compacto que ve el LLM en working_memory.store.delivery.zones[]. * Sin polygon (no le sirve al modelo), sí con costos/días/horas. */ export function buildZonesForLLM(deliveryZones) { return getZones(deliveryZones).map(zoneSummary); } /** * Resumen humano para system/replies cuando el LLM lo necesita en prosa. * Ejemplo: "Centro ($1500, lun-sab 10-20h), Palermo ($2000, mar/jue 11-19h)". */ export function summarizeDeliveryZones(deliveryZones) { const zones = getZones(deliveryZones); if (!zones.length) return ""; return zones.map((z) => { const cost = z.delivery_cost != null ? `$${Number(z.delivery_cost).toLocaleString("es-AR")}` : ""; const days = formatDaysList(z.delivery_days); const hours = z.delivery_hours?.start && z.delivery_hours?.end ? `${z.delivery_hours.start.slice(0, 5)}-${z.delivery_hours.end.slice(0, 5)}h` : ""; const tail = [cost, days, hours].filter(Boolean).join(" "); return tail ? `${z.name} (${tail})` : z.name; }).join(", "); } function formatDaysList(days) { if (!Array.isArray(days) || !days.length) return ""; // Detectar rango consecutivo lun-sab etc. const idx = days.map((d) => DAY_KEYS_SHORT.indexOf(d)).filter((i) => i >= 0).sort((a, b) => a - b); if (idx.length >= 3) { const isContiguous = idx.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1); if (isContiguous) return `${DAY_KEYS_SHORT[idx[0]]}-${DAY_KEYS_SHORT[idx[idx.length - 1]]}`; } return idx.map((i) => DAY_KEYS_SHORT[i]).join("/"); } /** * Construye variables de contexto de tienda. El bot ya no usa templates de * texto, pero workingMemory sigue tomando algunos de estos campos. */ export function buildStoreContextVars(storeConfig = {}) { const dayKey = todayShortKey(); const sched = storeConfig.schedule || {}; const pickupSlot = pickDaySlot(sched.pickup, dayKey); const storeHoursToday = formatDaySlot(pickupSlot) || ""; return { store_name: storeConfig.name || "", bot_name: storeConfig.botName || "", store_address: storeConfig.address || "", store_phone: storeConfig.phone || "", store_hours_today: storeHoursToday, pickup_hours_today: formatDaySlot(pickupSlot) || "", delivery_zones_summary: summarizeDeliveryZones(storeConfig.delivery_zones), }; } export const __test__ = { todayShortKey, pickDaySlot, formatDaySlot, formatDaysList, DAY_NAMES };