/** * La pregunta que decide el mapeo cita → oportunidad: * * MEDIDO: `POST /opportunities/` devuelve 400 OPPORTUNITY_NO_DUPLICATE con * `meta.existingId` cuando el contacto ya tiene una. * * ¿El bloqueo es sobre CUALQUIER oportunidad, o solo sobre las abiertas? De eso * depende todo: una clienta de spa vuelve muchas veces, y si el CRM solo admite * una oportunidad por contacto en toda su vida, entonces "una cita = una * oportunidad" es un modelo imposible y hay que reciclar la misma fila. */ import { loadEnv } from "../lib/env.ts"; import { ctxDesdeEnv } from "../crm/ctx.ts"; import { crmRequest } from "../crm/client.ts"; loadEnv(); const LOC = process.env.CRM_LOCATION_ID!; const CTX = ctxDesdeEnv(); const PIPELINE = "Mrclt4VzRZV1DI4Vbt5c"; const ETAPA_PRIMERA = "8063839c-fa73-419a-8b16-9606fe8e64c1"; const ETAPA_GANADO = "b91c1653-e785-43a8-8b54-f493205c9b5a"; const CONTACTO = "WzBTBaHkNnpmjMb1Avx3"; // el de prueba, ya creado const OPP = "IMkYdAkBowggN9aKVbfc"; const ok = (s: string) => console.log(` ✔ ${s}`); const fail = (s: string) => console.log(` ✗ ${s}`); async function crearOtra(nombre: string) { try { const r: any = await crmRequest("POST", "/opportunities/", { token: CTX.token, body: { pipelineId: PIPELINE, locationId: LOC, name: nombre, pipelineStageId: ETAPA_PRIMERA, status: "open", contactId: CONTACTO, monetaryValue: 400, }, }); return { creada: r?.opportunity?.id as string, error: null as any }; } catch (e: any) { return { creada: null, error: e }; } } async function main() { console.log("── 1. Cerrar la oportunidad existente como «won» (cita completada)"); await crmRequest("PUT", `/opportunities/${OPP}/status`, { token: CTX.token, body: { status: "won" } }); let r: any = await crmRequest("GET", `/opportunities/${OPP}`, { token: CTX.token }); if (r?.opportunity?.status === "won") ok(`releído status=won, etapa=${r.opportunity.pipelineStageId}`); else fail(`al releer status=${r?.opportunity?.status}`); console.log("\n── 2. ¿El PUT general mueve la etapa a «Ganado»?"); try { await crmRequest("PUT", `/opportunities/${OPP}`, { token: CTX.token, body: { pipelineId: PIPELINE, pipelineStageId: ETAPA_GANADO }, }); r = await crmRequest("GET", `/opportunities/${OPP}`, { token: CTX.token }); if (r?.opportunity?.pipelineStageId === ETAPA_GANADO) ok(`etapa movida a Ganado, status sigue en «${r.opportunity.status}»`); else fail(`al releer etapa=${r?.opportunity?.pipelineStageId}`); } catch (e: any) { fail(`${e.message} — ${JSON.stringify(e.body).slice(0, 250)}`); } console.log("\n── 3. LA PREGUNTA: ¿deja crear otra con la anterior ya cerrada?"); const segunda = await crearOtra("Manicura — Prueba AgendaMax (segunda visita)"); if (segunda.creada) { ok(`SÍ — creada id=${segunda.creada}`); console.log(" >> El bloqueo es solo sobre oportunidades ABIERTAS."); console.log(" >> Modelo viable: una cita = una oportunidad, cerrando la previa."); console.log("\n── 4. Y con esta abierta, ¿rechaza una tercera?"); const tercera = await crearOtra("Pedicura — Prueba AgendaMax (tercera)"); if (tercera.creada) { console.log(` ✔ también la creó (id=${tercera.creada})`); console.log(" >> Entonces el rechazo anterior fue por nombre/importe idénticos."); console.log(` Limpieza extra: DELETE /opportunities/${tercera.creada}`); } else { ok(`rechazada como se esperaba: ${tercera.error?.body?.code ?? tercera.error?.message}`); console.log(" >> CONFIRMADO: máximo UNA oportunidad abierta por contacto."); } console.log(`\n Limpieza: DELETE /opportunities/${segunda.creada}`); } else { fail(`NO — ${segunda.error?.body?.code ?? segunda.error?.message}`); console.log(" >> Grave: un contacto solo puede tener UNA oportunidad en toda su vida."); console.log(" >> Entonces la oportunidad no puede representar una cita, sino la"); console.log(" relación con la clienta, y se recicla en cada visita."); } console.log("\n── 5. Oportunidades del contacto, tal como las ve el CRM"); try { const l: any = await crmRequest("GET", `/contacts/${CONTACTO}/opportunities`, { token: CTX.token }); console.log( JSON.stringify( (l?.opportunities ?? []).map((o: any) => ({ id: o.id, name: o.name, status: o.status, monetaryValue: o.monetaryValue, })), null, 2 ) ); } catch (e: any) { fail(e.message); } } main().catch((e) => { console.error(e); process.exit(1); });