/** * Spike de ESCRITURA contra Bucéfalo CRM. Escribe de verdad en la subcuenta del * cliente, así que todo lo que crea lleva el tag `agendamax:prueba` y el correo * autorizado, y al final imprime cómo borrarlo. * * Regla que gobierna este archivo: **un 200 no es prueba de nada**. Cada * escritura se vuelve a leer desde la API antes de darla por buena. El proyecto * hermano pasó meses creyendo que escribía porque los tests estaban escritos * desde la implementación y no contra el contrato real. * * node scripts/run-tsx.mjs platform/scripts/crm-spike-write.ts */ 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 CORREO = process.env.CRM_TEST_EMAIL || "urieljareth@grupo-e3.com"; const PIPELINE = "Mrclt4VzRZV1DI4Vbt5c"; // "Standar", medido en el spike de lectura const ETAPA_PRIMERA = "8063839c-fa73-419a-8b16-9606fe8e64c1"; // 1er Contacto const ETAPA_GANADO = "b91c1653-e785-43a8-8b54-f493205c9b5a"; const ETAPA_PERDIDO = "04b28d7f-167b-4e97-af24-5a229f56b27f"; const marca = `agendamax-spike-${Date.now()}`; function ok(s: string) { console.log(` ✔ ${s}`); } function fail(s: string) { console.log(` ✗ ${s}`); } async function main() { console.log(`Subcuenta ${LOC} · correo de prueba ${CORREO}\n`); let contactId: string | null = null; let oppId: string | null = null; // ── 1. Crear contacto CON atribución UTM ──────────────────────────────── console.log("── POST /contacts/ (con attributionSource)"); try { const r: any = await crmRequest("POST", "/contacts/", { token: CTX.token, body: { locationId: LOC, firstName: "Prueba", lastName: "AgendaMax", email: CORREO, phone: "+524451052792", country: "MX", source: "AgendaMax", tags: ["agendamax:prueba"], attributionSource: { sessionSource: "Referral", utmSource: "agendamax", utmMedium: "plataforma", utmCampaign: marca, campaign: marca, // hay que mandar los dos: /contacts/search descarta utmCampaign medium: "form", referrer: "https://agendamax.consultoriae3.com", }, }, }); contactId = r?.contact?.id ?? null; ok(`creado id=${contactId}`); } catch (e: any) { if (e.status === 400 && e.body?.meta?.contactId) { contactId = e.body.meta.contactId; ok(`ya existía (400 con meta) id=${contactId} · campo=${e.body?.meta?.matchingField}`); console.log(" >> El rechazo de duplicado del CRM funciona: es idempotencia real."); } else { fail(e.message); } } // ── 2. RELEER el contacto: ¿persistió la atribución? ──────────────────── console.log("\n── GET /contacts/{id} — relectura (¿persistió el UTM?)"); if (contactId) { try { const r: any = await crmRequest("GET", `/contacts/${contactId}`, { token: CTX.token }); const c = r?.contact; console.log( JSON.stringify( { id: c?.id, email: c?.email, phone: c?.phone, tags: c?.tags, source: c?.source, attributionSource: c?.attributionSource, }, null, 2 ) ); const utm = c?.attributionSource?.utmCampaign || c?.attributionSource?.campaign; if (utm === marca) ok("la atribución persistió y se puede releer"); else fail(`la atribución NO coincide (esperaba ${marca}, leí ${utm})`); } catch (e: any) { fail(e.message); } } // ── 3. Crear oportunidad ──────────────────────────────────────────────── console.log("\n── POST /opportunities/ (cita en espera → status open)"); if (contactId) { try { const r: any = await crmRequest("POST", "/opportunities/", { token: CTX.token, body: { pipelineId: PIPELINE, locationId: LOC, name: "Extensiones de pestañas — Prueba AgendaMax", pipelineStageId: ETAPA_PRIMERA, status: "open", contactId, monetaryValue: 850, }, }); oppId = r?.opportunity?.id ?? null; ok(`creada id=${oppId}`); } catch (e: any) { fail(`${e.message} — ${JSON.stringify(e.body).slice(0, 300)}`); } } // ── 4. RELEER la oportunidad ──────────────────────────────────────────── console.log("\n── GET /opportunities/{id} — relectura"); if (oppId) { try { const r: any = await crmRequest("GET", `/opportunities/${oppId}`, { token: CTX.token }); const o = r?.opportunity; console.log( JSON.stringify( { id: o?.id, name: o?.name, status: o?.status, monetaryValue: o?.monetaryValue, pipelineStageId: o?.pipelineStageId, contactId: o?.contact?.id ?? o?.contactId, }, null, 2 ) ); if (o?.monetaryValue === 850) ok("el importe persistió"); else fail(`el importe NO persistió: leí ${o?.monetaryValue}`); } catch (e: any) { fail(e.message); } } // ── 5. Cambiar el estado a won (cita completada) ──────────────────────── // MEDIDO: /status NO acepta pipelineStageId (422 "should not exist"). Solo status. console.log("\n── PUT /opportunities/{id}/status — cita completada → won (solo status)"); if (oppId) { try { await crmRequest("PUT", `/opportunities/${oppId}/status`, { token: CTX.token, body: { status: "won" } }); const r: any = await crmRequest("GET", `/opportunities/${oppId}`, { token: CTX.token }); const o = r?.opportunity; if (o?.status === "won") ok(`releído: status=${o.status}, etapa=${o.pipelineStageId}`); else fail(`devolvió 200 pero al releer status=${o?.status}`); } catch (e: any) { fail(`${e.message} — ${JSON.stringify(e.body).slice(0, 300)}`); } } // ── 5b. ¿Se puede mover la etapa por el PUT general? ───────────────────── console.log("\n── PUT /opportunities/{id} — mover a la etapa «Ganado»"); if (oppId) { try { await crmRequest("PUT", `/opportunities/${oppId}`, { token: CTX.token, body: { pipelineId: PIPELINE, pipelineStageId: ETAPA_GANADO }, }); const r: any = await crmRequest("GET", `/opportunities/${oppId}`, { token: CTX.token }); const o = r?.opportunity; if (o?.pipelineStageId === ETAPA_GANADO) ok(`releído: etapa movida, status=${o.status}`); else fail(`al releer etapa=${o?.pipelineStageId}`); } catch (e: any) { fail(`${e.message} — ${JSON.stringify(e.body).slice(0, 300)}`); } } // ── 6. Volver a lost (cita cancelada) ─────────────────────────────────── console.log("\n── PUT /opportunities/{id}/status — cita cancelada → lost"); if (oppId) { try { await crmRequest("PUT", `/opportunities/${oppId}/status`, { token: CTX.token, body: { status: "lost" } }); await crmRequest("PUT", `/opportunities/${oppId}`, { token: CTX.token, body: { pipelineId: PIPELINE, pipelineStageId: ETAPA_PERDIDO }, }); const r: any = await crmRequest("GET", `/opportunities/${oppId}`, { token: CTX.token }); const o = r?.opportunity; if (o?.status === "lost") ok(`releído: status=lost, etapa=${o.pipelineStageId}`); else fail(`al releer status=${o?.status}`); } catch (e: any) { fail(e.message); } } // ── 7. Enviar un correo ───────────────────────────────────────────────── console.log("\n── POST /conversations/messages — correo de prueba"); if (contactId) { try { const r: any = await crmRequest("POST", "/conversations/messages", { token: CTX.token, body: { type: "Email", contactId, subject: "Prueba de integración AgendaMax ↔ Bucéfalo CRM", html: `
Mensaje de prueba enviado desde AgendaMax.
Marca: ${marca}