/** * Ejerce por primera vez la ESCRITURA de citas y servicios contra el CRM real. * * Ciclo completo y autolimpiable: crear → RELEER → borrar → confirmar que ya no * está. Nada queda en la subcuenta del cliente aunque el script falle a mitad: * lo creado se registra y se borra en el `finally`. * * Usa las funciones de producción (`crearCita`, `obtenerCita`, `publicarServicio`) * y no una implementación paralela: lo que se valida aquí es el código que va a * correr, no un primo suyo. * * node scripts/run-tsx.mjs platform/scripts/crm-spike-escritura-cita-servicio.ts */ import { pool } from "../db/pool.ts"; import { crmRequest, VERSION_CALENDARS } from "../crm/client.ts"; import { ctxDesdeEnv } from "../crm/ctx.ts"; import { crearCita, obtenerCita, listarCalendarios, listarPersonal, isoConDesplazamiento, } from "../crm/calendars.ts"; import { catalogoDelCrm } from "../crm/services.ts"; const CTX = ctxDesdeEnv(1); const CONTACTO_PRUEBA = "WzBTBaHkNnpmjMb1Avx3"; // el que ya lleva el tag agendamax:prueba const TZ = "America/Mexico_City"; const creados: { que: string; id: string; ruta: string }[] = []; let spaId = ""; let fallos = 0; const check = (nombre: string, ok: boolean, detalle = "") => { console.log(` ${ok ? "✔" : "✖"} ${nombre}${detalle ? ` — ${detalle}` : ""}`); if (!ok) fallos++; }; async function main() { // ── CITA ────────────────────────────────────────────────────────────────── console.log("\n══ ESCRITURA DE CITA AL CALENDARIO ══"); const cals = await listarCalendarios(CTX); const spa = cals.find((c) => /servicio spa/i.test(c.name)) ?? cals[0]; spaId = spa.id; console.log(` calendario: ${spa.name} (${spa.id})`); const personal = await listarPersonal(CTX); const quien = personal.find((p) => /recepci/i.test(p.name)) ?? personal[0]; console.log(` asignada a: ${quien.name} (${quien.id})`); // Una fecha lejana y a una hora inequívoca, para que se distinga de lo real. const inicio = new Date(Date.now() + 120 * 86400000); inicio.setUTCHours(20, 0, 0, 0); // 14:00 en México const fin = new Date(inicio.getTime() + 45 * 60000); const startTime = isoConDesplazamiento(inicio, TZ); const endTime = isoConDesplazamiento(fin, TZ); console.log(` ventana : ${startTime} → ${endTime}`); const { id: eventoId } = await crearCita(CTX, { calendarId: spa.id, contactId: CONTACTO_PRUEBA, startTime, endTime, title: "[agendamax:prueba] Cita de verificación — BORRAR", assignedUserId: quien.id, appointmentStatus: "confirmed", }); creados.push({ que: "cita", id: eventoId, ruta: `/calendars/events/${eventoId}` }); check("el CRM aceptó la cita y devolvió id", Boolean(eventoId), eventoId); // No se acepta el 200 como prueba: se relee. const releida = await obtenerCita(CTX, eventoId); check("la cita existe al releerla", Boolean(releida)); check( "el contacto es el correcto", releida?.contactId === CONTACTO_PRUEBA, releida?.contactId ?? "(sin contacto)" ); check( "la hora de pared coincide con la que escribimos", String(releida?.startTime ?? "").startsWith(startTime.slice(0, 16)), `escrita ${startTime} · releída ${releida?.startTime}` ); check( "el estado quedó confirmado", releida?.appointmentStatus === "confirmed", String(releida?.appointmentStatus) ); // ── SERVICIO ────────────────────────────────────────────────────────────── console.log("\n══ PUBLICACIÓN DE SERVICIO AL CATÁLOGO ══"); const antes = await catalogoDelCrm(CTX); console.log(` catálogo antes: ${antes.length} servicios`); const r = await crmRequest("POST", "/calendars/services/catalog", { token: CTX.token, version: VERSION_CALENDARS, body: { locationId: CTX.locationId, name: "[agendamax:prueba] Servicio de verificación", slug: `agendamax-prueba-${Date.now()}`, serviceDuration: 45, serviceDurationUnit: "mins", staff: [{ id: quien.id }], variations: [], }, }); const servicioId = r?.service?.id ?? r?.id; if (servicioId) { creados.push({ que: "servicio", id: servicioId, ruta: `/calendars/services/catalog/${servicioId}`, }); } check("el CRM aceptó el servicio y devolvió id", Boolean(servicioId), String(servicioId)); const despues = await catalogoDelCrm(CTX); check( "el servicio aparece al releer el catálogo", despues.some((s) => s.id === servicioId), `${antes.length} → ${despues.length} servicios` ); const nuevo = despues.find((s) => s.id === servicioId); check("con la duración que le pusimos", nuevo?.serviceDuration === 45, String(nuevo?.serviceDuration)); } try { await main(); } catch (e: any) { fallos++; console.error("\n✖ El sondeo falló:", e?.error ?? e?.message ?? e); if (e?.body) console.error(" cuerpo:", JSON.stringify(e.body).slice(0, 400)); } finally { // ── LIMPIEZA: pase lo que pase, no se deja nada en la subcuenta ─────────── console.log("\n══ LIMPIEZA ══"); for (const c of creados) { try { await crmRequest("DELETE", c.ruta, { token: CTX.token, version: VERSION_CALENDARS }); console.log(` ✔ ${c.que} ${c.id} borrada`); } catch (e: any) { fallos++; console.error(` ✖ NO se pudo borrar ${c.que} ${c.id}: ${e?.message ?? e}`); console.error(` BÓRRALA A MANO en el CRM.`); } } // Se confirma el borrado releyendo, no fiándose del 200. for (const c of creados) { if (c.que === "cita") { // MEDIDO: `GET /calendars/events/appointments/{id}` SIGUE devolviendo la // cita después de borrarla — es un borrado lógico. La comprobación fiable // es listar el rango del calendario, donde ya no aparece. const ahora = Date.now(); const ev = await crmRequest("GET", "/calendars/events", { token: CTX.token, version: VERSION_CALENDARS, query: { locationId: CTX.locationId, calendarId: spaId, startTime: String(ahora), endTime: String(ahora + 365 * 86400000), }, }); const sigue = (ev?.events ?? []).some((e: any) => e.id === c.id); check("la cita ya no aparece en el calendario", !sigue); } else { const cat = await catalogoDelCrm(CTX); check("el servicio ya no está en el catálogo", !cat.some((s) => s.id === c.id)); } } await pool.end(); console.log(`\n${fallos === 0 ? "Todo en verde y la subcuenta queda limpia" : `${fallos} fallos`}`); process.exit(fallos === 0 ? 0 : 1); }