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>
191 lines
6.5 KiB
JavaScript
191 lines
6.5 KiB
JavaScript
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.
|
|
|
|
export async function getSettings({ tenantId }) {
|
|
const sql = `
|
|
SELECT
|
|
id, tenant_id,
|
|
store_name, bot_name, store_address, store_phone,
|
|
pickup_enabled, pickup_days,
|
|
pickup_hours_start::text as pickup_hours_start,
|
|
pickup_hours_end::text as pickup_hours_end,
|
|
schedule,
|
|
delivery_zones,
|
|
created_at, updated_at
|
|
FROM tenant_settings
|
|
WHERE tenant_id = $1
|
|
LIMIT 1
|
|
`;
|
|
const { rows } = await pool.query(sql, [tenantId]);
|
|
return rows[0] || null;
|
|
}
|
|
|
|
export async function upsertSettings({ tenantId, settings }) {
|
|
const {
|
|
store_name,
|
|
bot_name,
|
|
store_address,
|
|
store_phone,
|
|
pickup_enabled,
|
|
pickup_days,
|
|
pickup_hours_start,
|
|
pickup_hours_end,
|
|
schedule,
|
|
delivery_zones,
|
|
} = settings;
|
|
|
|
const sql = `
|
|
INSERT INTO tenant_settings (
|
|
tenant_id, store_name, bot_name, store_address, store_phone,
|
|
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)
|
|
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),
|
|
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),
|
|
pickup_hours_end = COALESCE(EXCLUDED.pickup_hours_end, tenant_settings.pickup_hours_end),
|
|
schedule = COALESCE(EXCLUDED.schedule, tenant_settings.schedule),
|
|
delivery_zones = COALESCE(EXCLUDED.delivery_zones, tenant_settings.delivery_zones),
|
|
updated_at = NOW()
|
|
RETURNING
|
|
id, tenant_id,
|
|
store_name, bot_name, store_address, store_phone,
|
|
pickup_enabled, pickup_days,
|
|
pickup_hours_start::text as pickup_hours_start,
|
|
pickup_hours_end::text as pickup_hours_end,
|
|
schedule,
|
|
delivery_zones,
|
|
created_at, updated_at
|
|
`;
|
|
|
|
const params = [
|
|
tenantId,
|
|
store_name || null,
|
|
bot_name || null,
|
|
store_address || null,
|
|
store_phone || null,
|
|
pickup_enabled ?? null,
|
|
pickup_days || null,
|
|
pickup_hours_start || null,
|
|
pickup_hours_end || null,
|
|
schedule ? JSON.stringify(schedule) : null,
|
|
delivery_zones ? JSON.stringify(delivery_zones) : null,
|
|
];
|
|
|
|
const { rows } = await pool.query(sql, params);
|
|
return rows[0];
|
|
}
|
|
|
|
/**
|
|
* 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(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",
|
|
};
|
|
|
|
const groups = {};
|
|
for (const day of dayOrder) {
|
|
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: [] };
|
|
groups[key].days.push(day);
|
|
}
|
|
|
|
if (!Object.keys(groups).length) return "";
|
|
|
|
const parts = Object.values(groups).map((g) => {
|
|
const days = g.days;
|
|
let dayStr;
|
|
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);
|
|
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]];
|
|
}
|
|
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)}`;
|
|
}
|
|
|
|
/**
|
|
* Forma de la storeConfig que consume el agente y su workingMemory.
|
|
*/
|
|
export async function getStoreConfig({ tenantId }) {
|
|
const settings = await getSettings({ tenantId });
|
|
|
|
if (!settings) {
|
|
return {
|
|
name: "la carnicería",
|
|
botName: "Piaf",
|
|
hours: "",
|
|
address: "",
|
|
phone: "",
|
|
pickupHours: "",
|
|
schedule: null,
|
|
delivery_zones: {},
|
|
};
|
|
}
|
|
|
|
const schedule = settings.schedule || {};
|
|
|
|
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: settings.pickup_enabled ? pickupHours : "",
|
|
address: settings.store_address || "",
|
|
phone: settings.store_phone || "",
|
|
pickupHours,
|
|
pickupEnabled: settings.pickup_enabled,
|
|
schedule,
|
|
delivery_zones: settings.delivery_zones || {},
|
|
};
|
|
}
|