Limpiar legacy delivery_* + arreglar carga del mapa en shadow DOM
Backend cleanup (todo el delivery vive ahora en delivery_zones.zones[]):
- Migration drop columns delivery_enabled / delivery_days / delivery_hours_start /
delivery_hours_end / delivery_min_order y limpieza de schedule.delivery JSONB.
- settingsRepo: SELECT/INSERT/UPDATE sólo con campos vigentes, formatScheduleHours
trabaja sobre pickup.
- handlers/settings: defaults sin legacy, validateSchedule sólo para pickup,
validateDeliveryZones nuevo (estructura GeoJSON + días).
- seed_piaf_settings_and_replies + tenant_settings migrations alineadas: schedule
sólo tiene pickup, delivery_zones queda en {} para reconfigurar via UI.
Frontend cleanup:
- settings-crud: borrado el panel "Delivery (Envío a domicilio)" + minOrder,
toggle deliveryEnabled, y el grid schedule.delivery. collectScheduleFromInputs
ahora sólo procesa pickup. save() ya no envía delivery_enabled/min_order.
Fix mapa (no cargaba):
- zone-map-editor: los <link> a leaflet.css/leaflet-geoman.css se inyectaban en
document.head, que NO cruza el shadow DOM de settings-crud, por lo que las
reglas de Leaflet no aplicaban al div del mapa. Ahora los <link> se anclan
como hijos del propio web component; al estar en light DOM dentro del shadow
root del padre, sí aplican.
- Espera explícita a que el stylesheet cargue antes de instanciar L.map.
- ResizeObserver + invalidateSize() para cuando el contenedor cambia tamaño
(router muestra/oculta panel, tabs, etc).
Smoke E2E sin regresión: 1kg vacío + envío → location en Centro → "martes 12hs"
→ orden confirmada con $28.000 (producto + envío). 157/157 tests verde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,19 +3,20 @@ import { pool } from "../../shared/db/pool.js";
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Tenant Settings - CRUD
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Modelo actual:
|
||||
// - Datos del comercio: store_name, bot_name, store_address, store_phone.
|
||||
// - Pickup (retiro en tienda): pickup_enabled + pickup_days CSV +
|
||||
// pickup_hours_start/end TIME, y `schedule.pickup` JSONB para horario por día.
|
||||
// - Delivery: TODO vive en `delivery_zones.zones[]` (polígonos GeoJSON con
|
||||
// costo, días y rango horario por zona). El bot valida zona usando la
|
||||
// ubicación que el cliente comparte por WhatsApp.
|
||||
|
||||
/**
|
||||
* Obtiene la configuración del tenant
|
||||
*/
|
||||
export async function getSettings({ tenantId }) {
|
||||
const sql = `
|
||||
SELECT
|
||||
SELECT
|
||||
id, tenant_id,
|
||||
store_name, bot_name, store_address, store_phone,
|
||||
delivery_enabled, delivery_days,
|
||||
delivery_hours_start::text as delivery_hours_start,
|
||||
delivery_hours_end::text as delivery_hours_end,
|
||||
delivery_min_order,
|
||||
pickup_enabled, pickup_days,
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
@@ -30,20 +31,12 @@ export async function getSettings({ tenantId }) {
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea o actualiza la configuración del tenant (upsert)
|
||||
*/
|
||||
export async function upsertSettings({ tenantId, settings }) {
|
||||
const {
|
||||
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,
|
||||
@@ -55,21 +48,15 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
const sql = `
|
||||
INSERT INTO tenant_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, delivery_zones
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
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),
|
||||
store_address = COALESCE(EXCLUDED.store_address, tenant_settings.store_address),
|
||||
store_phone = COALESCE(EXCLUDED.store_phone, tenant_settings.store_phone),
|
||||
delivery_enabled = COALESCE(EXCLUDED.delivery_enabled, tenant_settings.delivery_enabled),
|
||||
delivery_days = COALESCE(EXCLUDED.delivery_days, tenant_settings.delivery_days),
|
||||
delivery_hours_start = COALESCE(EXCLUDED.delivery_hours_start, tenant_settings.delivery_hours_start),
|
||||
delivery_hours_end = COALESCE(EXCLUDED.delivery_hours_end, tenant_settings.delivery_hours_end),
|
||||
delivery_min_order = COALESCE(EXCLUDED.delivery_min_order, tenant_settings.delivery_min_order),
|
||||
pickup_enabled = COALESCE(EXCLUDED.pickup_enabled, tenant_settings.pickup_enabled),
|
||||
pickup_days = COALESCE(EXCLUDED.pickup_days, tenant_settings.pickup_days),
|
||||
pickup_hours_start = COALESCE(EXCLUDED.pickup_hours_start, tenant_settings.pickup_hours_start),
|
||||
@@ -77,13 +64,9 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
schedule = COALESCE(EXCLUDED.schedule, tenant_settings.schedule),
|
||||
delivery_zones = COALESCE(EXCLUDED.delivery_zones, tenant_settings.delivery_zones),
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
RETURNING
|
||||
id, tenant_id,
|
||||
store_name, bot_name, store_address, store_phone,
|
||||
delivery_enabled, delivery_days,
|
||||
delivery_hours_start::text as delivery_hours_start,
|
||||
delivery_hours_end::text as delivery_hours_end,
|
||||
delivery_min_order,
|
||||
pickup_enabled, pickup_days,
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
@@ -98,11 +81,6 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
bot_name || null,
|
||||
store_address || null,
|
||||
store_phone || null,
|
||||
delivery_enabled ?? null,
|
||||
delivery_days || null,
|
||||
delivery_hours_start || null,
|
||||
delivery_hours_end || null,
|
||||
delivery_min_order ?? null,
|
||||
pickup_enabled ?? null,
|
||||
pickup_days || null,
|
||||
pickup_hours_start || null,
|
||||
@@ -110,150 +88,103 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
schedule ? JSON.stringify(schedule) : null,
|
||||
delivery_zones ? JSON.stringify(delivery_zones) : null,
|
||||
];
|
||||
|
||||
const { rows } = await pool.query(sql, params);
|
||||
|
||||
const { rows } = await pool.query(sql, params);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatea horarios desde schedule JSONB para mostrar de forma natural
|
||||
* Agrupa días con mismos horarios: "Lun a Vie de 9 a 14hs, Sáb de 9 a 13hs"
|
||||
* Formatea schedule.pickup ({ lun: { start, end } }) en prosa para mostrar al
|
||||
* cliente. Agrupa días con mismos horarios: "Lun a Vie de 9 a 14hs".
|
||||
*/
|
||||
function formatScheduleHours(scheduleType, enabled) {
|
||||
if (!enabled || !scheduleType || typeof scheduleType !== "object") {
|
||||
function formatScheduleHours(scheduleObj, enabled) {
|
||||
if (!enabled || !scheduleObj || typeof scheduleObj !== "object") {
|
||||
return enabled === false ? "No disponible" : "";
|
||||
}
|
||||
|
||||
const dayOrder = ["lun", "mar", "mie", "jue", "vie", "sab", "dom"];
|
||||
const dayNames = {
|
||||
lun: "Lunes", mar: "Martes", mie: "Miércoles",
|
||||
jue: "Jueves", vie: "Viernes", sab: "Sábado", dom: "Domingo"
|
||||
lun: "Lunes", mar: "Martes", mie: "Miércoles",
|
||||
jue: "Jueves", vie: "Viernes", sab: "Sábado", dom: "Domingo",
|
||||
};
|
||||
|
||||
// Agrupar días por horario
|
||||
const groups = {};
|
||||
for (const day of dayOrder) {
|
||||
const slot = scheduleType[day];
|
||||
const slot = scheduleObj[day];
|
||||
if (!slot || !slot.start || !slot.end) continue;
|
||||
|
||||
const key = `${slot.start}-${slot.end}`;
|
||||
if (!groups[key]) {
|
||||
groups[key] = { start: slot.start, end: slot.end, days: [] };
|
||||
}
|
||||
if (!groups[key]) groups[key] = { start: slot.start, end: slot.end, days: [] };
|
||||
groups[key].days.push(day);
|
||||
}
|
||||
|
||||
if (Object.keys(groups).length === 0) return "";
|
||||
if (!Object.keys(groups).length) return "";
|
||||
|
||||
// Formatear cada grupo
|
||||
const parts = Object.values(groups).map(g => {
|
||||
const parts = Object.values(groups).map((g) => {
|
||||
const days = g.days;
|
||||
let dayStr;
|
||||
|
||||
// Detectar rangos consecutivos
|
||||
if (days.length >= 3) {
|
||||
const indices = days.map(d => dayOrder.indexOf(d));
|
||||
const isConsecutive = indices.every((v, i, arr) => i === 0 || v === arr[i-1] + 1);
|
||||
if (isConsecutive) {
|
||||
dayStr = `${dayNames[days[0]]} a ${dayNames[days[days.length-1]]}`;
|
||||
} else {
|
||||
dayStr = days.map(d => dayNames[d]).join(", ");
|
||||
}
|
||||
const indices = days.map((d) => dayOrder.indexOf(d));
|
||||
const isConsecutive = indices.every((v, i, arr) => i === 0 || v === arr[i - 1] + 1);
|
||||
dayStr = isConsecutive
|
||||
? `${dayNames[days[0]]} a ${dayNames[days[days.length - 1]]}`
|
||||
: days.map((d) => dayNames[d]).join(", ");
|
||||
} else if (days.length === 2) {
|
||||
dayStr = `${dayNames[days[0]]} y ${dayNames[days[1]]}`;
|
||||
} else {
|
||||
dayStr = dayNames[days[0]];
|
||||
}
|
||||
|
||||
const startH = g.start.slice(0, 5);
|
||||
const endH = g.end.slice(0, 5);
|
||||
return `${dayStr} de ${startH} a ${endH}`;
|
||||
return `${dayStr} de ${g.start.slice(0, 5)} a ${g.end.slice(0, 5)}`;
|
||||
});
|
||||
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
function formatLegacyPickupHours(enabled, days, start, end) {
|
||||
if (!enabled) return "No disponible";
|
||||
if (!days || !start || !end) return "";
|
||||
const daysFormatted = days.split(",").map((d) => d.trim()).join(", ");
|
||||
return `${daysFormatted} de ${start.slice(0, 5)} a ${end.slice(0, 5)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración formateada para usar en prompts (storeConfig)
|
||||
* Forma de la storeConfig que consume el agente y su workingMemory.
|
||||
*/
|
||||
export async function getStoreConfig({ tenantId }) {
|
||||
const settings = await getSettings({ tenantId });
|
||||
|
||||
|
||||
if (!settings) {
|
||||
// Valores por defecto si no hay configuración
|
||||
return {
|
||||
name: "la carnicería",
|
||||
botName: "Piaf",
|
||||
hours: "",
|
||||
address: "",
|
||||
phone: "",
|
||||
deliveryHours: "",
|
||||
pickupHours: "",
|
||||
schedule: null,
|
||||
delivery_zones: {},
|
||||
};
|
||||
}
|
||||
|
||||
const schedule = settings.schedule || {};
|
||||
|
||||
// Usar nuevo formato schedule si existe, sino legacy
|
||||
let deliveryHours, pickupHours;
|
||||
|
||||
if (schedule.delivery && Object.keys(schedule.delivery).length > 0) {
|
||||
deliveryHours = formatScheduleHours(schedule.delivery, settings.delivery_enabled);
|
||||
} else {
|
||||
// Legacy format
|
||||
deliveryHours = formatLegacyHours(
|
||||
settings.delivery_enabled,
|
||||
settings.delivery_days,
|
||||
settings.delivery_hours_start,
|
||||
settings.delivery_hours_end
|
||||
);
|
||||
}
|
||||
|
||||
if (schedule.pickup && Object.keys(schedule.pickup).length > 0) {
|
||||
pickupHours = formatScheduleHours(schedule.pickup, settings.pickup_enabled);
|
||||
} else {
|
||||
// Legacy format
|
||||
pickupHours = formatLegacyHours(
|
||||
settings.pickup_enabled,
|
||||
settings.pickup_days,
|
||||
settings.pickup_hours_start,
|
||||
settings.pickup_hours_end
|
||||
);
|
||||
}
|
||||
|
||||
// Combinar horarios para store_hours (usa pickup como horario de tienda)
|
||||
let storeHours = "";
|
||||
if (settings.pickup_enabled) {
|
||||
storeHours = pickupHours;
|
||||
}
|
||||
const pickupHours = schedule.pickup && Object.keys(schedule.pickup).length
|
||||
? formatScheduleHours(schedule.pickup, settings.pickup_enabled)
|
||||
: formatLegacyPickupHours(
|
||||
settings.pickup_enabled,
|
||||
settings.pickup_days,
|
||||
settings.pickup_hours_start,
|
||||
settings.pickup_hours_end,
|
||||
);
|
||||
|
||||
return {
|
||||
name: settings.store_name || "la carnicería",
|
||||
botName: settings.bot_name || "Piaf",
|
||||
hours: storeHours,
|
||||
hours: settings.pickup_enabled ? pickupHours : "",
|
||||
address: settings.store_address || "",
|
||||
phone: settings.store_phone || "",
|
||||
deliveryHours,
|
||||
pickupHours,
|
||||
deliveryEnabled: settings.delivery_enabled,
|
||||
pickupEnabled: settings.pickup_enabled,
|
||||
schedule,
|
||||
delivery_zones: settings.delivery_zones || {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatear horarios en formato legacy (días + rango único)
|
||||
*/
|
||||
function formatLegacyHours(enabled, days, start, end) {
|
||||
if (!enabled) return "No disponible";
|
||||
if (!days || !start || !end) return "";
|
||||
|
||||
const daysFormatted = days.split(",").map(d => d.trim()).join(", ");
|
||||
const startFormatted = start?.slice(0, 5) || "";
|
||||
const endFormatted = end?.slice(0, 5) || "";
|
||||
|
||||
return `${daysFormatted} de ${startFormatted} a ${endFormatted}`;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,42 @@
|
||||
import { getSettings, upsertSettings, getStoreConfig } from "../db/settingsRepo.js";
|
||||
|
||||
// Días de la semana para validación
|
||||
const VALID_DAYS = ["lun", "mar", "mie", "jue", "vie", "sab", "dom"];
|
||||
|
||||
/**
|
||||
* Genera schedule por defecto con horarios uniformes
|
||||
*/
|
||||
function createDefaultSchedule() {
|
||||
const defaultDays = ["lun", "mar", "mie", "jue", "vie", "sab"];
|
||||
const delivery = {};
|
||||
function defaultPickupSchedule() {
|
||||
const days = ["lun", "mar", "mie", "jue", "vie", "sab"];
|
||||
const pickup = {};
|
||||
|
||||
for (const day of defaultDays) {
|
||||
delivery[day] = { start: "09:00", end: "18:00" };
|
||||
pickup[day] = { start: "08:00", end: "20:00" };
|
||||
}
|
||||
|
||||
return { delivery, pickup };
|
||||
for (const d of days) pickup[d] = { start: "08:00", end: "20:00" };
|
||||
return { pickup };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración actual del tenant
|
||||
*/
|
||||
export async function handleGetSettings({ tenantId }) {
|
||||
const settings = await getSettings({ tenantId });
|
||||
|
||||
// Si no hay configuración, devolver defaults
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
store_name: "Mi Negocio",
|
||||
bot_name: "Piaf",
|
||||
store_address: "",
|
||||
store_phone: "",
|
||||
delivery_enabled: true,
|
||||
delivery_days: "lun,mar,mie,jue,vie,sab",
|
||||
delivery_hours_start: "09:00",
|
||||
delivery_hours_end: "18:00",
|
||||
delivery_min_order: 0,
|
||||
pickup_enabled: true,
|
||||
pickup_days: "lun,mar,mie,jue,vie,sab",
|
||||
pickup_hours_start: "08:00",
|
||||
pickup_hours_end: "20:00",
|
||||
schedule: createDefaultSchedule(),
|
||||
schedule: defaultPickupSchedule(),
|
||||
delivery_zones: {},
|
||||
is_default: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Si no tiene schedule, generar desde datos legacy
|
||||
// Si schedule está vacío pero tenemos los campos legacy de pickup, generar
|
||||
// schedule.pickup para que la UI pueda editar el grid por día.
|
||||
let schedule = settings.schedule;
|
||||
if (!schedule || Object.keys(schedule).length === 0) {
|
||||
schedule = buildScheduleFromLegacy(settings);
|
||||
if (!schedule || !schedule.pickup || !Object.keys(schedule.pickup).length) {
|
||||
schedule = buildPickupScheduleFromLegacy(settings);
|
||||
}
|
||||
|
||||
return {
|
||||
...settings,
|
||||
// Formatear horarios TIME a HH:MM
|
||||
delivery_hours_start: settings.delivery_hours_start?.slice(0, 5) || "09:00",
|
||||
delivery_hours_end: settings.delivery_hours_end?.slice(0, 5) || "18:00",
|
||||
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,
|
||||
@@ -66,171 +45,95 @@ export async function handleGetSettings({ tenantId }) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye schedule desde datos legacy
|
||||
*/
|
||||
function buildScheduleFromLegacy(settings) {
|
||||
const schedule = { delivery: {}, pickup: {} };
|
||||
|
||||
// Delivery
|
||||
if (settings.delivery_enabled && settings.delivery_days) {
|
||||
const days = settings.delivery_days.split(",").map(d => d.trim());
|
||||
const start = settings.delivery_hours_start?.slice(0, 5) || "09:00";
|
||||
const end = settings.delivery_hours_end?.slice(0, 5) || "18:00";
|
||||
for (const day of days) {
|
||||
if (VALID_DAYS.includes(day)) {
|
||||
schedule.delivery[day] = { start, end };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pickup
|
||||
if (settings.pickup_enabled && settings.pickup_days) {
|
||||
const days = settings.pickup_days.split(",").map(d => d.trim());
|
||||
function buildPickupScheduleFromLegacy(settings) {
|
||||
const out = { pickup: {} };
|
||||
if (settings?.pickup_enabled && settings?.pickup_days) {
|
||||
const days = settings.pickup_days.split(",").map((d) => d.trim());
|
||||
const start = settings.pickup_hours_start?.slice(0, 5) || "08:00";
|
||||
const end = settings.pickup_hours_end?.slice(0, 5) || "20:00";
|
||||
for (const day of days) {
|
||||
if (VALID_DAYS.includes(day)) {
|
||||
schedule.pickup[day] = { start, end };
|
||||
}
|
||||
if (VALID_DAYS.includes(day)) out.pickup[day] = { start, end };
|
||||
}
|
||||
}
|
||||
|
||||
return schedule;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida la estructura del schedule
|
||||
*/
|
||||
function validateSchedule(schedule) {
|
||||
if (!schedule || typeof schedule !== "object") return;
|
||||
|
||||
// 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];
|
||||
if (!typeSchedule || typeof typeSchedule !== "object") continue;
|
||||
|
||||
for (const [day, slot] of Object.entries(typeSchedule)) {
|
||||
if (!VALID_DAYS.includes(day)) {
|
||||
throw new Error(`Invalid day in schedule.${type}: ${day}`);
|
||||
}
|
||||
|
||||
if (slot === null) continue; // null = no disponible
|
||||
|
||||
if (typeof slot !== "object" || !slot.start || !slot.end) {
|
||||
throw new Error(`Invalid slot format for ${type}.${day}`);
|
||||
}
|
||||
|
||||
if (!timeRegex.test(slot.start)) {
|
||||
throw new Error(`Invalid start time for ${type}.${day}: ${slot.start}`);
|
||||
}
|
||||
if (!timeRegex.test(slot.end)) {
|
||||
throw new Error(`Invalid end time for ${type}.${day}: ${slot.end}`);
|
||||
const pickup = schedule.pickup;
|
||||
if (!pickup || typeof pickup !== "object") return;
|
||||
for (const [day, slot] of Object.entries(pickup)) {
|
||||
if (!VALID_DAYS.includes(day)) throw new Error(`Invalid day in schedule.pickup: ${day}`);
|
||||
if (slot === null) continue;
|
||||
if (typeof slot !== "object" || !slot.start || !slot.end) {
|
||||
throw new Error(`Invalid slot format for pickup.${day}`);
|
||||
}
|
||||
if (!timeRegex.test(slot.start)) throw new Error(`Invalid start time for pickup.${day}: ${slot.start}`);
|
||||
if (!timeRegex.test(slot.end)) throw new Error(`Invalid end time for pickup.${day}: ${slot.end}`);
|
||||
}
|
||||
}
|
||||
|
||||
function syncPickupLegacyFromSchedule(settings) {
|
||||
const pickup = settings?.schedule?.pickup;
|
||||
if (!pickup) return;
|
||||
const days = Object.keys(pickup).filter((d) => pickup[d] && pickup[d].start && pickup[d].end);
|
||||
days.sort((a, b) => VALID_DAYS.indexOf(a) - VALID_DAYS.indexOf(b));
|
||||
if (days.length) {
|
||||
settings.pickup_days = days.join(",");
|
||||
const first = pickup[days[0]];
|
||||
settings.pickup_hours_start = first.start;
|
||||
settings.pickup_hours_end = first.end;
|
||||
} else {
|
||||
settings.pickup_days = "";
|
||||
}
|
||||
}
|
||||
|
||||
function validateDeliveryZones(dz) {
|
||||
if (!dz || typeof dz !== "object") return;
|
||||
if (dz.zones && !Array.isArray(dz.zones)) {
|
||||
throw new Error("delivery_zones.zones must be an array");
|
||||
}
|
||||
for (const z of dz.zones || []) {
|
||||
if (!z?.id || !z?.name) throw new Error("Each zone needs id + name");
|
||||
if (z.polygon && (z.polygon.type !== "Polygon" || !Array.isArray(z.polygon.coordinates))) {
|
||||
throw new Error(`Invalid polygon GeoJSON for zone ${z.id}`);
|
||||
}
|
||||
if (Array.isArray(z.delivery_days)) {
|
||||
for (const d of z.delivery_days) {
|
||||
if (!VALID_DAYS.includes(d)) throw new Error(`Invalid delivery day in zone ${z.id}: ${d}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza campos legacy desde schedule
|
||||
*/
|
||||
function syncLegacyFromSchedule(settings) {
|
||||
const schedule = settings.schedule;
|
||||
if (!schedule) return;
|
||||
|
||||
// Sincronizar delivery
|
||||
if (schedule.delivery) {
|
||||
const deliveryDays = Object.keys(schedule.delivery).filter(d => schedule.delivery[d] !== null);
|
||||
if (deliveryDays.length > 0) {
|
||||
// Ordenar días
|
||||
deliveryDays.sort((a, b) => VALID_DAYS.indexOf(a) - VALID_DAYS.indexOf(b));
|
||||
settings.delivery_days = deliveryDays.join(",");
|
||||
|
||||
// Usar primer horario como legacy
|
||||
const firstSlot = schedule.delivery[deliveryDays[0]];
|
||||
if (firstSlot) {
|
||||
settings.delivery_hours_start = firstSlot.start;
|
||||
settings.delivery_hours_end = firstSlot.end;
|
||||
}
|
||||
} else {
|
||||
settings.delivery_days = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Sincronizar pickup
|
||||
if (schedule.pickup) {
|
||||
const pickupDays = Object.keys(schedule.pickup).filter(d => schedule.pickup[d] !== null);
|
||||
if (pickupDays.length > 0) {
|
||||
pickupDays.sort((a, b) => VALID_DAYS.indexOf(a) - VALID_DAYS.indexOf(b));
|
||||
settings.pickup_days = pickupDays.join(",");
|
||||
|
||||
const firstSlot = schedule.pickup[pickupDays[0]];
|
||||
if (firstSlot) {
|
||||
settings.pickup_hours_start = firstSlot.start;
|
||||
settings.pickup_hours_end = firstSlot.end;
|
||||
}
|
||||
} else {
|
||||
settings.pickup_days = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda la configuración del tenant
|
||||
*/
|
||||
export async function handleSaveSettings({ tenantId, settings }) {
|
||||
// Validaciones básicas
|
||||
if (!settings.store_name?.trim()) {
|
||||
throw new Error("store_name is required");
|
||||
}
|
||||
if (!settings.bot_name?.trim()) {
|
||||
throw new Error("bot_name is required");
|
||||
}
|
||||
if (!settings.store_name?.trim()) throw new Error("store_name is required");
|
||||
if (!settings.bot_name?.trim()) throw new Error("bot_name is required");
|
||||
|
||||
// Validar schedule si viene
|
||||
if (settings.schedule) {
|
||||
validateSchedule(settings.schedule);
|
||||
// Sincronizar campos legacy desde schedule
|
||||
syncLegacyFromSchedule(settings);
|
||||
} else {
|
||||
// Legacy: validar días individuales
|
||||
if (settings.delivery_days) {
|
||||
const days = settings.delivery_days.split(",").map(d => d.trim().toLowerCase());
|
||||
for (const day of days) {
|
||||
if (!VALID_DAYS.includes(day)) {
|
||||
throw new Error(`Invalid delivery day: ${day}`);
|
||||
}
|
||||
}
|
||||
settings.delivery_days = days.join(",");
|
||||
}
|
||||
syncPickupLegacyFromSchedule(settings);
|
||||
}
|
||||
|
||||
if (settings.pickup_days) {
|
||||
const days = settings.pickup_days.split(",").map(d => d.trim().toLowerCase());
|
||||
for (const day of days) {
|
||||
if (!VALID_DAYS.includes(day)) {
|
||||
throw new Error(`Invalid pickup day: ${day}`);
|
||||
}
|
||||
}
|
||||
settings.pickup_days = days.join(",");
|
||||
}
|
||||
if (settings.delivery_zones) {
|
||||
validateDeliveryZones(settings.delivery_zones);
|
||||
}
|
||||
|
||||
// Validar horarios legacy
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||
|
||||
if (settings.delivery_hours_start && !timeRegex.test(settings.delivery_hours_start)) {
|
||||
throw new Error("Invalid delivery_hours_start format (use HH:MM)");
|
||||
}
|
||||
if (settings.delivery_hours_end && !timeRegex.test(settings.delivery_hours_end)) {
|
||||
throw new Error("Invalid delivery_hours_end format (use HH:MM)");
|
||||
}
|
||||
if (settings.pickup_hours_start && !timeRegex.test(settings.pickup_hours_start)) {
|
||||
throw new Error("Invalid pickup_hours_start format (use HH:MM)");
|
||||
}
|
||||
if (settings.pickup_hours_end && !timeRegex.test(settings.pickup_hours_end)) {
|
||||
throw new Error("Invalid pickup_hours_end format (use HH:MM)");
|
||||
if (settings.pickup_days) {
|
||||
const days = settings.pickup_days.split(",").map((d) => d.trim().toLowerCase());
|
||||
for (const d of days) {
|
||||
if (!VALID_DAYS.includes(d)) throw new Error(`Invalid pickup day: ${d}`);
|
||||
}
|
||||
settings.pickup_days = days.join(",");
|
||||
}
|
||||
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
|
||||
if (settings.pickup_hours_start && !timeRegex.test(settings.pickup_hours_start)) {
|
||||
throw new Error("Invalid pickup_hours_start format (use HH:MM)");
|
||||
}
|
||||
if (settings.pickup_hours_end && !timeRegex.test(settings.pickup_hours_end)) {
|
||||
throw new Error("Invalid pickup_hours_end format (use HH:MM)");
|
||||
}
|
||||
|
||||
const result = await upsertSettings({ tenantId, settings });
|
||||
@@ -239,8 +142,6 @@ export async function handleSaveSettings({ tenantId, settings }) {
|
||||
ok: true,
|
||||
settings: {
|
||||
...result,
|
||||
delivery_hours_start: result.delivery_hours_start?.slice(0, 5),
|
||||
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 || {},
|
||||
@@ -249,9 +150,6 @@ export async function handleSaveSettings({ tenantId, settings }) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el storeConfig formateado para prompts
|
||||
*/
|
||||
export async function handleGetStoreConfig({ tenantId }) {
|
||||
return await getStoreConfig({ tenantId });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user