import { pool } from "../db/pool.ts"; import { ctxDe } from "./ctx.ts"; import { obtenerConexion, etapasDe } from "./connection.ts"; import { resolverContacto } from "./contacts.ts"; import { upsertOportunidad, estadoOportunidad, nombreOportunidad, } from "./opportunities.ts"; export interface ResultadoProyeccion { appointmentId: number; crmContactId: string; crmOpportunityId: string; status: string; via: string; } /** * Proyecta UNA cita al CRM como oportunidad. * * Nombre `SERVICIO — CLIENTA`, importe el del servicio, y estado según la cita: * en espera → `open`, completada → `won`, cancelada o no asistió → `lost`. * * Resuelve primero el contacto: `POST /calendars/...` y `POST /opportunities/` * exigen `contactId`, así que una clienta nacida en la plataforma tiene que * existir en el CRM antes de que su cita pueda salir. */ export async function proyectarCita( businessId: number, appointmentId: number ): Promise { const conexion = await obtenerConexion(businessId); if (!conexion) { throw Object.assign(new Error("Este negocio no tiene conexión con Bucéfalo CRM"), { status: 409, }); } // La conexión da pipeline y etapas; el contexto da la credencial. Son dos // cosas distintas y por eso se piden por separado. const ctx = await ctxDe(businessId); const { rows } = await pool.query( `SELECT a.id, a.status, a.price, a.crm_opportunity_id, c.id AS client_id, c.name AS client_name, c.phone, c.email, c.crm_contact_id, s.name AS service_name FROM appointments a JOIN clients c ON c.id = a.client_id JOIN services s ON s.id = a.service_id WHERE a.id = $1 AND a.business_id = $2`, [appointmentId, businessId] ); const cita = rows[0]; if (!cita) { throw Object.assign(new Error("Cita no encontrada"), { status: 404 }); } const contacto = await resolverContacto(ctx, { crmContactId: cita.crm_contact_id, phone: cita.phone, email: cita.email, name: cita.client_name, source: "AgendaMax", tags: ["agendamax"], }); // Se ancla el contacto en cuanto se conoce: si la oportunidad falla después, // al menos la clienta ya no se volverá a crear duplicada. if (contacto.contact.id !== cita.crm_contact_id) { await pool.query( `UPDATE clients SET crm_contact_id = $2, crm_synced_at = now() WHERE id = $1`, [cita.client_id, contacto.contact.id] ); } const estado = estadoOportunidad(cita.status); const oportunidad = await upsertOportunidad(ctx, { contactId: contacto.contact.id, nombre: nombreOportunidad(cita.service_name, cita.client_name), importe: Number(cita.price) || 0, estado, etapas: etapasDe(conexion), oportunidadId: cita.crm_opportunity_id, }); await pool.query( `UPDATE appointments SET crm_opportunity_id = $2, crm_status = $3, crm_synced_at = now() WHERE id = $1`, [appointmentId, oportunidad.opportunity.id, estado] ); return { appointmentId, crmContactId: contacto.contact.id, crmOpportunityId: oportunidad.opportunity.id, status: estado, via: `contacto:${contacto.via} · oportunidad:${oportunidad.via}`, }; }