import { test } from "node:test"; import assert from "node:assert/strict"; import { pool } from "../db/pool.ts"; import { resetDb, crearNegocio } from "./helpers.ts"; import { upsertConversacion, upsertMensaje } from "../crm/syncConversations.ts"; test("upsertConversacion es idempotente: dos veces no duplica", async () => { await resetDb(); const b = await crearNegocio(); const conv = { id: "conv-1", contactId: "c-1", fullName: "Ana", lastMessageBody: "hola", lastMessageType: "TYPE_SMS", lastMessageDate: 1756000000000, unreadCount: 2, }; const id1 = await upsertConversacion(b.id, conv as any); const id2 = await upsertConversacion(b.id, conv as any); assert.equal(id1, id2); const { rows } = await pool.query<{ n: number }>( `SELECT count(*)::int AS n FROM conversations WHERE business_id = $1`, [b.id] ); assert.equal(rows[0].n, 1); }); test("upsertConversacion enlaza con la clienta local por crm_contact_id", async () => { await resetDb(); const b = await crearNegocio(); const { rows: cl } = await pool.query( `INSERT INTO clients (business_id, name, crm_contact_id) VALUES ($1,'Ana','c-9') RETURNING id`, [b.id] ); await upsertConversacion(b.id, { id: "conv-9", contactId: "c-9", fullName: "Ana" } as any); const { rows } = await pool.query( `SELECT client_id FROM conversations WHERE business_id = $1 AND crm_conversation_id = 'conv-9'`, [b.id] ); assert.equal(rows[0].client_id, cl[0].id); }); test("el enlace con la clienta se rellena después, y no se pierde al resincronizar", async () => { await resetDb(); const b = await crearNegocio(); // Primero llega la conversación, cuando la clienta todavía no existe. await upsertConversacion(b.id, { id: "conv-x", contactId: "c-x", fullName: "Ana" } as any); const { rows: sin } = await pool.query( `SELECT client_id FROM conversations WHERE crm_conversation_id = 'conv-x'` ); assert.equal(sin[0].client_id, null); // Luego la sincronización de contactos crea la clienta… await pool.query( `INSERT INTO clients (business_id, name, crm_contact_id) VALUES ($1,'Ana','c-x')`, [b.id] ); await upsertConversacion(b.id, { id: "conv-x", contactId: "c-x", fullName: "Ana" } as any); const { rows: con } = await pool.query( `SELECT client_id FROM conversations WHERE crm_conversation_id = 'conv-x'` ); assert.ok(con[0].client_id, "al resincronizar debe quedar enlazada"); // …y una tercera pasada NO puede desenlazarla. await pool.query(`UPDATE clients SET crm_contact_id = NULL WHERE business_id = $1`, [b.id]); await upsertConversacion(b.id, { id: "conv-x", contactId: "c-x", fullName: "Ana" } as any); const { rows: sigue } = await pool.query( `SELECT client_id FROM conversations WHERE crm_conversation_id = 'conv-x'` ); assert.equal(sigue[0].client_id, con[0].client_id, "un enlace resuelto no se borra"); }); test("upsertMensaje no duplica el mismo crm_message_id", async () => { await resetDb(); const b = await crearNegocio(); const convId = await upsertConversacion(b.id, { id: "conv-2", contactId: "c-2" } as any); const m = { id: "msg-1", body: "hola", direction: "inbound", messageType: "TYPE_SMS" }; await upsertMensaje(b.id, convId, m as any); await upsertMensaje(b.id, convId, m as any); const { rows } = await pool.query<{ n: number }>( `SELECT count(*)::int AS n FROM messages WHERE business_id = $1`, [b.id] ); assert.equal(rows[0].n, 1); }); test("upsertMensaje guarda el canal normalizado y el crudo", async () => { await resetDb(); const b = await crearNegocio(); const convId = await upsertConversacion(b.id, { id: "conv-3" } as any); await upsertMensaje(b.id, convId, { id: "msg-2", body: "x", direction: "outbound", messageType: "TYPE_EMAIL", } as any); const { rows } = await pool.query( `SELECT channel, channel_raw, direction FROM messages WHERE crm_message_id = 'msg-2'` ); assert.equal(rows[0].channel, "Email"); assert.equal(rows[0].channel_raw, "TYPE_EMAIL", "el valor original se conserva"); assert.equal(rows[0].direction, "outbound"); }); test("un mensaje con dirección desconocida se guarda como entrante, no revienta", async () => { await resetDb(); const b = await crearNegocio(); const convId = await upsertConversacion(b.id, { id: "conv-4" } as any); await upsertMensaje(b.id, convId, { id: "msg-3", body: "x" } as any); const { rows } = await pool.query( `SELECT direction FROM messages WHERE crm_message_id = 'msg-3'` ); assert.equal(rows[0].direction, "inbound"); }); test("sincronizar un hilo por id NO degrada el nombre ni el canal ya conocidos", async () => { await resetDb(); const b = await crearNegocio(); // Primero llega desde el buscador, con nombre y canal buenos. await upsertConversacion(b.id, { id: "conv-deg", contactId: "c-d", fullName: "Carmen García", lastMessageType: "TYPE_INSTAGRAM", lastMessageBody: "hola", } as any); // Luego llega por id, que no trae ni nombre ni canal reconocible. await upsertConversacion(b.id, { id: "conv-deg", contactId: "c-d" } as any); const { rows } = await pool.query( `SELECT contact_name, last_message_type, last_message_body FROM conversations WHERE crm_conversation_id = 'conv-deg'` ); assert.equal(rows[0].contact_name, "Carmen García", "el nombre bueno se conserva"); assert.equal(rows[0].last_message_type, "Instagram", "el canal bueno se conserva"); assert.equal(rows[0].last_message_body, "hola", "y el último mensaje también"); }); test("un nombre nuevo y bueno SÍ reemplaza al anterior", async () => { await resetDb(); const b = await crearNegocio(); await upsertConversacion(b.id, { id: "conv-n", fullName: "Nombre Viejo" } as any); await upsertConversacion(b.id, { id: "conv-n", fullName: "Nombre Nuevo" } as any); const { rows } = await pool.query( `SELECT contact_name FROM conversations WHERE crm_conversation_id = 'conv-n'` ); assert.equal(rows[0].contact_name, "Nombre Nuevo"); });