corregidos bugs de: ret, vs delivery, efectivo vs link, charsets, price query
This commit is contained in:
@@ -19,6 +19,7 @@ export async function getSettings({ tenantId }) {
|
||||
pickup_enabled, pickup_days,
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
schedule,
|
||||
created_at, updated_at
|
||||
FROM tenant_settings
|
||||
WHERE tenant_id = $1
|
||||
@@ -46,15 +47,17 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_days,
|
||||
pickup_hours_start,
|
||||
pickup_hours_end,
|
||||
schedule,
|
||||
} = 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
|
||||
pickup_enabled, pickup_days, pickup_hours_start, pickup_hours_end,
|
||||
schedule
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
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),
|
||||
@@ -69,6 +72,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_days = COALESCE(EXCLUDED.pickup_days, tenant_settings.pickup_days),
|
||||
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),
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
id, tenant_id,
|
||||
@@ -80,6 +84,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_enabled, pickup_days,
|
||||
pickup_hours_start::text as pickup_hours_start,
|
||||
pickup_hours_end::text as pickup_hours_end,
|
||||
schedule,
|
||||
created_at, updated_at
|
||||
`;
|
||||
|
||||
@@ -98,6 +103,7 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
pickup_days || null,
|
||||
pickup_hours_start || null,
|
||||
pickup_hours_end || null,
|
||||
schedule ? JSON.stringify(schedule) : null,
|
||||
];
|
||||
|
||||
const { rows } = await pool.query(sql, params);
|
||||
@@ -105,6 +111,64 @@ export async function upsertSettings({ tenantId, settings }) {
|
||||
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"
|
||||
*/
|
||||
function formatScheduleHours(scheduleType, enabled) {
|
||||
if (!enabled || !scheduleType || typeof scheduleType !== "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"
|
||||
};
|
||||
|
||||
// Agrupar días por horario
|
||||
const groups = {};
|
||||
for (const day of dayOrder) {
|
||||
const slot = scheduleType[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: [] };
|
||||
}
|
||||
groups[key].days.push(day);
|
||||
}
|
||||
|
||||
if (Object.keys(groups).length === 0) return "";
|
||||
|
||||
// Formatear cada grupo
|
||||
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(", ");
|
||||
}
|
||||
} 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 parts.join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración formateada para usar en prompts (storeConfig)
|
||||
*/
|
||||
@@ -121,39 +185,43 @@ export async function getStoreConfig({ tenantId }) {
|
||||
phone: "",
|
||||
deliveryHours: "",
|
||||
pickupHours: "",
|
||||
schedule: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Formatear horarios para mostrar
|
||||
const formatHours = (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}`;
|
||||
};
|
||||
const schedule = settings.schedule || {};
|
||||
|
||||
const deliveryHours = formatHours(
|
||||
settings.delivery_enabled,
|
||||
settings.delivery_days,
|
||||
settings.delivery_hours_start,
|
||||
settings.delivery_hours_end
|
||||
);
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
const pickupHours = formatHours(
|
||||
settings.pickup_enabled,
|
||||
settings.pickup_days,
|
||||
settings.pickup_hours_start,
|
||||
settings.pickup_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
|
||||
// Combinar horarios para store_hours (usa pickup como horario de tienda)
|
||||
let storeHours = "";
|
||||
if (settings.pickup_enabled && settings.pickup_days) {
|
||||
storeHours = `${settings.pickup_days.split(",").join(", ")} ${settings.pickup_hours_start?.slice(0,5) || ""}-${settings.pickup_hours_end?.slice(0,5) || ""}`;
|
||||
if (settings.pickup_enabled) {
|
||||
storeHours = pickupHours;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -166,5 +234,24 @@ export async function getStoreConfig({ tenantId }) {
|
||||
pickupHours,
|
||||
deliveryEnabled: settings.delivery_enabled,
|
||||
pickupEnabled: settings.pickup_enabled,
|
||||
schedule,
|
||||
// Campos legacy para compatibilidad
|
||||
delivery_days: settings.delivery_days,
|
||||
delivery_hours_start: settings.delivery_hours_start,
|
||||
delivery_hours_end: settings.delivery_hours_end,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@ import { getSettings, upsertSettings, getStoreConfig } from "../db/settingsRepo.
|
||||
// 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 = {};
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la configuración actual del tenant
|
||||
*/
|
||||
@@ -25,10 +41,17 @@ export async function handleGetSettings({ tenantId }) {
|
||||
pickup_days: "lun,mar,mie,jue,vie,sab",
|
||||
pickup_hours_start: "08:00",
|
||||
pickup_hours_end: "20:00",
|
||||
schedule: createDefaultSchedule(),
|
||||
is_default: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Si no tiene schedule, generar desde datos legacy
|
||||
let schedule = settings.schedule;
|
||||
if (!schedule || Object.keys(schedule).length === 0) {
|
||||
schedule = buildScheduleFromLegacy(settings);
|
||||
}
|
||||
|
||||
return {
|
||||
...settings,
|
||||
// Formatear horarios TIME a HH:MM
|
||||
@@ -36,10 +59,121 @@ export async function handleGetSettings({ tenantId }) {
|
||||
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,
|
||||
is_default: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida la estructura del schedule
|
||||
*/
|
||||
function validateSchedule(schedule) {
|
||||
if (!schedule || typeof schedule !== "object") return;
|
||||
|
||||
const timeRegex = /^([01]?[0-9]|2[0-3]):[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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -52,41 +186,48 @@ export async function handleSaveSettings({ tenantId, settings }) {
|
||||
throw new Error("bot_name is required");
|
||||
}
|
||||
|
||||
// Validar días
|
||||
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}`);
|
||||
// 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(",");
|
||||
}
|
||||
settings.delivery_days = days.join(",");
|
||||
}
|
||||
|
||||
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}`);
|
||||
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(",");
|
||||
}
|
||||
settings.pickup_days = days.join(",");
|
||||
}
|
||||
|
||||
// Validar horarios
|
||||
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)");
|
||||
// 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)");
|
||||
}
|
||||
}
|
||||
|
||||
const result = await upsertSettings({ tenantId, settings });
|
||||
|
||||
Reference in New Issue
Block a user