{"openapi":"3.0.0","info":{"title":"Inventory WS API","version":"1.0.0","description":"Documentación completa de la API del sistema de inventario","contact":{"name":"API Support","email":"support@inventory-ws.com"}},"servers":[{"url":"https://inventario.miterreno.mx","description":"Servidor de producción"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}}},"Project":{"type":"object","properties":{"project_id":{"type":"integer"},"project_name":{"type":"string"},"project_description":{"type":"string"},"category":{"type":"string"},"owner_id":{"type":"integer"},"start_date":{"type":"string","format":"date-time"},"created_on":{"type":"string","format":"date-time"},"project_status_id":{"type":"integer"},"code":{"type":"string"},"landingpage":{"type":"string"}}},"User":{"type":"object","properties":{"user_id":{"type":"integer"},"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string","format":"email"}}},"Contract":{"type":"object","properties":{"contract_id":{"type":"integer"},"project_id":{"type":"integer"},"property_id":{"type":"integer"},"customer_id":{"type":"integer"},"agent_id":{"type":"integer"},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"Property":{"type":"object","properties":{"property_id":{"type":"integer"},"property_name":{"type":"string"},"property_type":{"type":"string"},"status":{"type":"string"},"price":{"type":"number"},"project_id":{"type":"integer"}}}}},"tags":[{"name":"Autenticación","description":"Login, logout y recuperación de contraseña"},{"name":"Usuarios","description":"Gestión de usuarios"},{"name":"Proyectos","description":"Operaciones relacionadas con proyectos"},{"name":"Propiedades","description":"Gestión de propiedades"},{"name":"Contratos","description":"Gestión de contratos"},{"name":"Financiamientos","description":"Gestión de financiamientos"},{"name":"Pagos de Financiamiento","description":"Pagos de financiamientos"},{"name":"Agentes","description":"Gestión de agentes"},{"name":"Leads / CRM","description":"Leads y gestión CRM"},{"name":"Pipeline CRM","description":"Pipeline de ventas CRM"},{"name":"Archivos","description":"Gestión de archivos"},{"name":"Reportes","description":"Generación de reportes"},{"name":"Reservaciones","description":"Reservaciones de propiedades"},{"name":"Notaría","description":"Operaciones de notaría"},{"name":"Agenda de Firmas","description":"Gestión de agenda de firmas"},{"name":"Cierres","description":"Cierres de ventas"},{"name":"Transacciones Financieras","description":"Transacciones financieras"},{"name":"Movimientos Bancarios","description":"Movimientos bancarios"},{"name":"Conciliación Bancaria","description":"Conciliación bancaria"},{"name":"Pagos Masivos","description":"Procesamiento de pagos masivos"},{"name":"OpenPay","description":"Integración con OpenPay"},{"name":"WhatsApp","description":"Integración con WhatsApp"},{"name":"Webhooks","description":"Webhooks del sistema"},{"name":"Catálogos","description":"Catálogos del sistema"},{"name":"Roles","description":"Gestión de roles"},{"name":"Landing Pages","description":"Landing pages de proyectos"},{"name":"Tracking","description":"Tracking de clicks y conversiones"},{"name":"Solicitudes de Empleo","description":"Gestión de solicitudes de empleo"},{"name":"Comisiones Propietario","description":"Comisiones de propietario"},{"name":"Cron Jobs","description":"Tareas programadas"},{"name":"Query","description":"Consultas directas"},{"name":"Documentación","description":"Documentación de la API"},{"name":"MCP","description":"Model Context Protocol"},{"name":"OAuth","description":"OAuth / Well-known"},{"name":"Admin","description":"Operaciones administrativas generales"},{"name":"Admin - Comisiones","description":"Administración de comisiones"},{"name":"Admin - Financiamientos","description":"Administración de financiamientos"},{"name":"Admin - Pagos","description":"Administración de pagos"},{"name":"Admin - Propiedades","description":"Administración de propiedades"},{"name":"Admin - Proyectos","description":"Administración de proyectos"},{"name":"Admin - Clientes","description":"Administración de clientes"},{"name":"Admin - Reportes","description":"Reportes administrativos"},{"name":"Admin - Encuestas","description":"Administración de encuestas"},{"name":"Admin - Solicitudes de Empleo","description":"Administración de solicitudes de empleo"},{"name":"Admin - Comisiones Propietario","description":"Administración de comisiones de propietario"},{"name":"Admin - Pólizas","description":"Administración de pólizas"},{"name":"Admin - Cobranza","description":"Métricas de cobranza"},{"name":"Agente Virtual","description":"API para agente virtual"}],"paths":{"/api/admin/{payment_id}/cancel":{"put":{"summary":"Cancelar","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/{payment_id}":{"put":{"summary":"Actualizar appadmin","tags":["Admin"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/cobranza-metrics":{"get":{"summary":"Métricas de cobranza","tags":["Admin - Cobranza"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/comment-moderation/{comment_id}":{"put":{"summary":"Ocultar o mostrar un comentario manualmente (humano en el circuito)","tags":["Leads / CRM"],"description":"Aplica is_hidden en Facebook para ese comentario y registra la acción en la bitácora ('oculto-manual' / 'visible-manual'). Funciona también en modo observación: permite moderar a mano lo que la IA marcó.\n","parameters":[{"in":"path","name":"comment_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hidden":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Acción aplicada"},"400":{"description":"Solicitud inválida"},"404":{"description":"Comentario no está en la bitácora"},"500":{"description":"Error interno del servidor"},"502":{"description":"Facebook rechazó el cambio"}}}},"/api/admin/comment-moderation":{"get":{"summary":"Bitácora de moderación de comentarios (pantalla de Marketing)","tags":["Leads / CRM"],"description":"Comentarios clasificados por la IA (NEGATIVO/INTERES/NEUTRO/ERROR) con la acción tomada, más el modo vigente (observacion/activo) y contadores por etiqueta. Filtro opcional por label. Incluye también el rastro de la RESPUESTA PRIVADA de cada comentario (private_reply_at / _psid / _lead_id / _error) y su modo (private_reply_mode: off|observacion|activo): sin esas columnas el modo 'observacion' —que existe justo para calibrar antes de encender— escribía su simulación en la bitácora y nadie podía leerla desde la app.\n","parameters":[{"in":"query","name":"label","required":false,"schema":{"type":"string"}},{"in":"query","name":"limit","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions/{commission_id}/invoice-number":{"put":{"summary":"Actualizar número de factura","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions/{commission_id}/payments":{"post":{"summary":"Crear pagos","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions/{commission_id}":{"get":{"summary":"Obtener comisiones por ID","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar comisiones","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions/{commission_id}/status":{"put":{"summary":"Actualizar estado","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions/generate":{"get":{"summary":"Preview de generación de comisiones","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Generar comisiones","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/commissions":{"get":{"summary":"Listar comisiones","tags":["Admin - Comisiones"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/crm-analytics":{"get":{"summary":"Analítica de leads por formulario y anuncio (CRM + costos de Meta)","tags":["Leads / CRM"],"description":"Agregados del CRM (leads, temperaturas, score, serios) por formulario y anuncio, enriquecidos con Insights de Meta (gasto, impresiones, clicks). Data-driven: formularios y anuncios nuevos aparecen solos.\n","responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/customers":{"get":{"summary":"Listar clientes","tags":["Admin - Clientes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/gb-properties":{"get":{"summary":"Catálogo de propiedades de intermediación (sistema del anunciante)","tags":["Leads / CRM"],"description":"Reenvía la lista de propiedades de la API del anunciante (Golden Brokers) para que el panel de Campañas pueda ofrecer un selector al ligar un anuncio con una propiedad. Solo lectura y sin guardar nada: de una propiedad ajena este CRM únicamente almacena su código. Si la API del anunciante no contesta, devuelve 200 con propiedades [] y ok=false — el panel lo dice y el resto de la pantalla sigue igual.\n","responses":{"200":{"description":"Catálogo (posiblemente vacío si el anunciante no contestó)"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/job-applications/{id}/notes/{noteId}":{"delete":{"summary":"Eliminar notas","tags":["Admin - Solicitudes de Empleo"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"noteId","required":true,"schema":{"type":"string"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/job-applications/{id}/notes":{"post":{"summary":"Crear notas","tags":["Admin - Solicitudes de Empleo"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/job-applications/{id}":{"get":{"summary":"Obtener job-applications por ID","tags":["Admin - Solicitudes de Empleo"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar job-applications","tags":["Admin - Solicitudes de Empleo"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/job-applications/{id}/stage":{"patch":{"summary":"Cambiar etapa de solicitud","tags":["Admin - Solicitudes de Empleo"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/job-applications":{"get":{"summary":"Listar job-applications","tags":["Admin - Solicitudes de Empleo"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/leads":{"get":{"summary":"Listar TODOS los leads activos (vista de administración del CRM)","tags":["Leads / CRM"],"description":"Igual que /api/agents/{agent_id}/leads pero sin filtrar por agente e incluyendo los datos del agente asignado. Alimenta el kanban de administración y el de Marketing. Sin `page_id` responde los leads de Mi Terreno (incluidos los que no traen página), que es lo que devolvió siempre.\n","parameters":[{"in":"query","name":"page_id","schema":{"type":"string"},"description":"Marca (meta_page) cuyo tablero se pide."},{"in":"query","name":"user_id","schema":{"type":"integer"},"description":"Usuario que consulta. Si viene y NO tiene acceso a `page_id` en user_page_access, se responde 403.\n"}],"responses":{"200":{"description":"Operación exitosa"},"403":{"description":"El usuario no tiene acceso a esa marca"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mesa-ayuda-settings":{"get":{"summary":"Reglas configurables de la Mesa de ayuda (horario y auto-respuesta)","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"Devuelve la ÚNICA fila de `mesa_ayuda_settings` —horario de atención, minutos sin respuesta antes de escalar, tolerancia de apertura, interruptor y texto de la auto-respuesta—, quién la actualizó por última vez y los rangos válidos (`limites`) para que la pantalla valide con los MISMOS números que el servidor.\nLee de la base SIN pasar por la caché del módulo compartido: la pantalla de configuración tiene que mostrar lo que está guardado, no lo que una instancia trae en memoria.\nSi la fila no existiera (alguien la borró), responde los valores de respaldo con `origen: \"respaldo\"` — los mismos que usa el sistema mientras tanto, para que la pantalla no muestre campos vacíos.\nRequiere rol Administración (3) o Marketing (15).\n","responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Falta identificar al usuario que consulta"},"403":{"description":"El usuario no puede configurar la Mesa de ayuda"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Guardar las reglas de la Mesa de ayuda","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"Reemplaza la fila completa. TODAS las claves son obligatorias aunque no cambien: con un cuerpo parcial, un campo olvidado volvería a su valor viejo (o al de fábrica) sin que nadie lo notara, y aquí se decide a qué hora escala la mesa y qué se le contesta al cliente.\nValidación estricta ANTES de tocar nada, con los MISMOS rangos que los CHECK de la tabla: 0 ≤ hora_inicio &lt; hora_fin ≤ 24, minutos_sin_respuesta 1–240, tolerancia_apertura_min 0–240, auto_reply_activa booleano y auto_reply_texto no vacío (máximo el límite de Instagram, el canal más estrecho por el que sale). Cualquier problema es 400 y no se escribe nada.\nSella `updated_by`/`updated_at` con el usuario actuante e invalida la caché de esta instancia; las demás la refrescan solas en menos de un minuto (ver el aviso de serverless en app/utilis/mesaSettings.js).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hora_inicio","hora_fin","minutos_sin_respuesta","tolerancia_apertura_min","auto_reply_activa","auto_reply_texto"],"properties":{"hora_inicio":{"type":"integer","description":"Hora de apertura (0-24), hora local de Mazatlán"},"hora_fin":{"type":"integer","description":"Hora de cierre exclusiva (0-24); 24 = hasta las 23:59"},"minutos_sin_respuesta":{"type":"integer","description":"Minutos de espera antes de escalar (1-240)"},"tolerancia_apertura_min":{"type":"integer","description":"Minutos tras abrir en los que no se escala (0-240)"},"auto_reply_activa":{"type":"boolean"},"auto_reply_texto":{"type":"string"},"user_id":{"type":"integer","description":"respaldo de identidad mientras la cookie no viaje"}}}}}},"responses":{"200":{"description":"Configuración guardada"},"400":{"description":"Solicitud inválida (no se escribió nada)"},"403":{"description":"El usuario no puede configurar la Mesa de ayuda"},"409":{"description":"Otro usuario guardó la configuración al mismo tiempo"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mesa-rotacion-proyecto":{"get":{"summary":"Todas las listas de rotación de la Mesa de ayuda (por proyecto)","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"Un solo GET con todo lo que necesita la pestaña Marketing → Configuración → Mesa de ayuda, sin N+1:\n- `listas`: la lista GENERAL (project_id null) y la de cada proyecto que\n  tenga una propia. Cada lista trae sus `usuarios` en orden de\n  `position` (con `nombre`, `activo` y `tiene_rol` para poder marcar a\n  quien ya se desactivó o perdió el rol Mesa de ayuda) y la copia\n  (`cc_user_id`, `cc_nombre`, `cc_activo`). La GENERAL siempre viene,\n  aunque estuviera vacía.\n- `proyectos_disponibles`: proyectos que TODAVÍA no tienen lista propia\n  (los que hoy heredan la GENERAL), para el selector de \"crear lista\".\n- `miembros`: catálogo de usuarios ACTIVOS con el rol Mesa de ayuda\n  (role_code 17); son los únicos que el PUT acepta dentro de una lista.\n\nRequiere un usuario con rol Administración (3) o Marketing (15): quién atiende los leads reales no es información pública.\n","responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Falta identificar al usuario que consulta"},"403":{"description":"El usuario no puede configurar la Mesa de ayuda"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Guardar UNA lista de rotación (la GENERAL o la de un proyecto)","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"Reemplaza por completo la lista del grupo indicado numerando las posiciones 1..N sin huecos (por legibilidad: el escalamiento resuelve sus destinos entre los habilitados, así que un hueco ya no lo apaga) y hace upsert de la copia. Todo va en UN SOLO statement con CTE — el DELETE se drena antes del INSERT — así que o queda la lista nueva completa o no cambia nada.\nproject_id null = lista GENERAL (leads sin proyecto y proyectos sin lista propia). La clave debe venir EXPLÍCITA: omitirla reescribiría la GENERAL sin que nadie lo pidiera. Lo mismo con cc_user_id: mandar null para dejar la lista sin copia.\nValidación estricta ANTES de tocar nada; ante cualquier problema responde 400 y no escribe: el arreglo no puede ir vacío (apagaría el reparto y el escalamiento de ese proyecto), los user_id deben ser enteros positivos, sin repetidos, existentes, ACTIVOS y con el rol Mesa de ayuda; cc_user_id null o un usuario existente; project_id null o un proyecto existente.\nRESCATE DE LEADS: en el MISMO statement, a quien SALE de la lista se le quitan los leads que el cron de la Mesa de ayuda SÍ escalaría y pasan a la posición 1 de la lista nueva, con su evento 'assigned' en el historial. El predicado es EL MISMO del cron (activos, del ámbito de esta lista, dentro del piso de recencia, sin respuesta saliente real, y o bien con conversación entrante o bien de formulario posterior al corte de despliegue y sin ninguna señal de contacto); si no fuera idéntico, o se moverían leads que el cron nunca habría tocado —quitándole el cliente a su asesor— o se quedarían pegados a alguien fuera de la lista, donde el cron no los ve y dejan de escalar en silencio. No se envía WhatsApp por esto. Cuando hubo rescate, la respuesta trae `leads_reasignados` y `reasignados_a` (y el `message` ya lo dice).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["project_id","user_ids","cc_user_id"],"properties":{"project_id":{"type":"integer","nullable":true,"description":"null = lista GENERAL"},"user_ids":{"type":"array","description":"user_id EN ORDEN; el primero es la posición 1","items":{"type":"integer"}},"cc_user_id":{"type":"integer","nullable":true,"description":"usuario en copia de esta lista (null = sin copia)"},"user_id":{"type":"integer","description":"respaldo de identidad mientras la cookie no viaje"}}}}}},"responses":{"200":{"description":"Lista guardada"},"400":{"description":"Solicitud inválida (no se escribió nada)"},"403":{"description":"El usuario no puede configurar la Mesa de ayuda"},"409":{"description":"Otro usuario guardó la misma lista al mismo tiempo"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Borrar la lista propia de un proyecto (vuelve a heredar la GENERAL)","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"Borra la lista y la copia del proyecto indicado; a partir de ahí sus leads los atiende la lista GENERAL. La lista GENERAL NO se puede borrar (es el respaldo de todos): responde 400. El project_id se lee del body y, si no viene, de ?project_id=.\nRESCATE DE LEADS (motivo 'borrado_de_lista'): borrar la lista propia de un proyecto orfana exactamente igual que sacar a alguien de ella en el PUT —quien estaba SOLO en la lista borrada deja de coincidir con la lista efectiva del lead (que pasa a ser la GENERAL) y sus leads se caen del cron: sin escalar, sin alerta y sin aviso—, así que se aplica el MISMO rescate, con el MISMO predicado, dentro del mismo statement: esos leads pasan al primer usuario HABILITADO de la lista GENERAL. Si hubiera leads que rescatar y la GENERAL no tuviera un solo usuario habilitado, no hay a dónde moverlos y el borrado se rechaza con 409 sin tocar nada.\n","parameters":[{"in":"query","name":"project_id","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Lista borrada"},"400":{"description":"Solicitud inválida (incluye el intento de borrar la GENERAL)"},"403":{"description":"El usuario no puede configurar la Mesa de ayuda"},"404":{"description":"El proyecto no tenía lista propia"},"409":{"description":"Hay leads que rescatar y la lista GENERAL no tiene a nadie habilitado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads/{meta_ad_id}/preview":{"get":{"summary":"Vista previa del anuncio (iframe de Meta)","tags":["Leads / CRM"],"description":"Pide a Graph la vista previa del anuncio y devuelve el src del iframe que Meta genera. El token nunca llega al navegador: por eso esto es un endpoint y no una llamada directa del panel. Los links de preview de Meta CADUCAN (~24 h), así que se generan al momento en cada apertura del modal, nunca se guardan.\n","parameters":[{"in":"path","name":"meta_ad_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"format","required":false,"schema":{"type":"string","enum":["MOBILE_FEED_STANDARD","INSTAGRAM_STANDARD","DESKTOP_FEED_STANDARD"]},"description":"Formato de la vista previa (default MOBILE_FEED_STANDARD)"}],"responses":{"200":{"description":"Operación exitosa ({success, src, format})"},"400":{"description":"Solicitud inválida"},"404":{"description":"El anuncio no está registrado en el panel"},"500":{"description":"Error interno del servidor"},"502":{"description":"Graph no pudo generar la vista previa"},"503":{"description":"Falta configurar el token de Meta"}}}},"/api/admin/meta-ads/{meta_ad_id}/project":{"put":{"summary":"Vincular (o desvincular) un anuncio de Meta con un proyecto","tags":["Leads / CRM"],"description":"Asigna el proyecto al que pertenece la campaña del anuncio. Los leads nuevos de ese anuncio heredan el project_id al crearse (respuestas rápidas correctas, kanban y análisis con proyecto). Al vincular, los leads existentes del anuncio SIN proyecto se backfillean con el nuevo. project_id null desvincula el anuncio (los leads existentes no se tocan).\n","parameters":[{"in":"path","name":"meta_ad_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"type":"integer","nullable":true},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Proyecto vinculado"},"400":{"description":"Solicitud inválida"},"404":{"description":"Anuncio no registrado en el CRM"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads/{meta_ad_id}/property":{"put":{"summary":"Ligar (o desligar) un anuncio con una propiedad de intermediación","tags":["Leads / CRM"],"description":"Guarda en el anuncio el CÓDIGO de la propiedad ajena que promociona (property_code del sistema del anunciante, p. ej. GB-2026-01-004). Con eso, el botón de IA del chat consulta la ficha en vivo y llena los huecos de precio y medidas. Aquí NO se guarda precio ni descripción: solo el apuntador. gb_property_code null desliga el anuncio. Antes de guardar comprueba que el código exista en la API del anunciante; si esa API no contesta, guarda igual y avisa con verificado=false — un sistema ajeno caído no puede impedir que aquí se configure un anuncio.\n","parameters":[{"in":"path","name":"meta_ad_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"gb_property_code":{"type":"string","nullable":true},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Propiedad ligada (o desligada)"},"400":{"description":"Solicitud inválida, o el código no existe en el inventario del anunciante"},"404":{"description":"Anuncio no registrado en el CRM"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads/{meta_ad_id}":{"put":{"summary":"Renombrar un anuncio de Meta (alias legible)","tags":["Leads / CRM"],"description":"Asigna el alias que se muestra en el kanban y el detalle del lead (p. ej. \"Leads Foráneos\"). Hace upsert por si el anuncio aún no existe en el catálogo.\n","parameters":[{"in":"path","name":"meta_ad_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ad_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Anuncio renombrado"},"400":{"description":"Solicitud inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads/{meta_ad_id}/status":{"put":{"summary":"Pausar o reanudar un anuncio de Meta desde el CRM","tags":["Leads / CRM"],"description":"Cambia el estado del anuncio en Meta (status=PAUSED detiene la entrega y el gasto; status=ACTIVE la reanuda) vía Graph API con el token de sistema, y sincroniza el effective_status resultante en meta_lead_ads. Solo opera sobre anuncios ya registrados en el catálogo del CRM.\n","parameters":[{"in":"path","name":"meta_ad_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["PAUSED","ACTIVE"]}}}}}},"responses":{"200":{"description":"Estado cambiado"},"400":{"description":"Solicitud inválida"},"404":{"description":"Anuncio no registrado en el CRM"},"500":{"description":"Error interno del servidor"},"502":{"description":"Meta rechazó el cambio"}}}},"/api/admin/meta-ads/campaigns/{campaign_id}/project":{"put":{"summary":"Vincular (o desvincular) una campaña completa de Meta con un proyecto","tags":["Leads / CRM"],"description":"Asigna el proyecto a TODOS los anuncios de la campaña (para el árbol campaña→conjunto→anuncio del panel de configuración). Los leads nuevos de esos anuncios heredan el project_id al crearse. Al vincular, los leads existentes de esos anuncios SIN proyecto se backfillean con el nuevo. project_id null desvincula todos los anuncios de la campaña (los leads existentes no se tocan).\n","parameters":[{"in":"path","name":"campaign_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"type":"integer","nullable":true},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Proyecto vinculado a la campaña"},"400":{"description":"Solicitud inválida"},"404":{"description":"Campaña sin anuncios registrados en el CRM"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads/campaigns/{campaign_id}/scope":{"put":{"summary":"Clasificar una campaña por ámbito (propiedades u operaciones)","tags":["Leads / CRM"],"description":"Guarda el ámbito MANUAL de una campaña en meta_campaign_scope (upsert). Pensado para campañas de páginas ajenas al CRM (Golden Brokers, etc.): su inventario vive en otra plataforma y aquí no hay dato que las clasifique, así que el panel lo pide con un selector por campaña. En Mi Terreno MX el ámbito se deduce solo del project_id de sus anuncios y este valor no se usa. Sin renglón guardado el GET /api/admin/meta-ads devuelve campaign_scope null y el frontend aplica el default 'propiedades'.\n","parameters":[{"in":"path","name":"campaign_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scope","user_id"],"properties":{"scope":{"type":"string","enum":["propiedades","operaciones"]},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Ámbito guardado"},"400":{"description":"Solicitud inválida (scope fuera del catálogo, etc.)"},"404":{"description":"Campaña sin anuncios registrados en el CRM"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-ads":{"get":{"summary":"Listar anuncios de Meta con su proyecto y su página (ámbitos)","tags":["Leads / CRM"],"description":"Para el panel de configuración (árbol campaña→conjunto→anuncio): antes de listar, sincroniza la cuenta publicitaria COMPLETA desde la Marketing API (best-effort), así los anuncios sin leads también aparecen y se les puede asignar PROYECTO antes del primer lead. Cada anuncio va con su proyecto, el total de leads recibidos y el estado + metadatos (nombre real, campaña, conjunto) frescos de Meta y auto-reparados en BD.\nÁMBITOS POR PÁGINA (2026-08-14): la cuenta corre campañas de VARIAS páginas de Facebook, no solo de la del CRM, y desde aquí se listan TODAS. Cada anuncio trae page_id (página dueña del creativo; null si Graph no la reveló), page_name (nombre legible de las páginas conocidas; desconocida = 'Página ' + id; null si page_id es null) y es_pagina_crm (true si page_id es null o es la página del CRM — null cuenta como CRM a propósito, fail-open: ocultar de más escondería anuncios reales de Mi Terreno MX). El frontend agrupa las secciones con es_pagina_crm/page_id, muestra el selector de proyecto SOLO en la página del CRM y avisa que los mensajes de otras páginas llegan al inbox de esa página en Meta, no al CRM (webhooks: fase 2).\nÁMBITO MANUAL POR CAMPAÑA (2026-08-14, segunda iteración): cada anuncio trae además campaign_scope ('propiedades' | 'operaciones' | null), el ámbito guardado a mano para su campaña en meta_campaign_scope vía PUT /campaigns/{id}/scope. Solo aplica a campañas de páginas ajenas (en Mi Terreno MX el ámbito se deduce del project_id de los anuncios); null = sin clasificar y el frontend aplica el default 'propiedades'.\nPROPIEDADES DE INTERMEDIACIÓN (2026-08-31): cada anuncio trae además gb_property_code, el código de la propiedad AJENA que promociona (inventario que no vive en este CRM; se liga con PUT /api/admin/meta-ads/{id}/property). Aquí viaja SOLO el código: el catálogo con títulos y precios se pide aparte, a GET /api/admin/gb-properties, para que este endpoint NO se quede esperando a un sistema ajeno — el árbol de campañas tiene que pintarse aunque el anunciante esté caído.\nLa respuesta incluye sync_ok (y sync_error si la sincronización falló) y ad_account_id: la cuenta publicitaria sin prefijo act_ (o null si no se pudo resolver) con la que el frontend arma los links a Ads Manager.\nQUIÉN ATIENDE EL LEAD YA NO SALE DE AQUÍ (2026-07-29): sale de la lista del PROYECTO del anuncio (mesa_rotation_project / mesa_project_config, Marketing → Configuración → Mesa de ayuda). Por eso esta respuesta dejó de traer `agents`, `cc_user_id`/`cc_name` y `campaign_config`: eran el pool por anuncio (meta_ad_agents), la copia por anuncio (meta_lead_ads.cc_user_id) y el pool/copia por campaña (meta_campaign_agents / meta_campaign_config) — tablas ya muertas que nadie leía y cuyos endpoints de escritura se retiraron. Las columnas y tablas se conservan por historial, pero no se exponen. La asignación por formulario se retiró 2026-07-24.\n","responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-daily-spend":{"get":{"summary":"Gasto diario por campaña (Meta Insights)","tags":["Leads / CRM"],"description":"Devuelve el gasto de cada campaña día por día, listo para graficar. El eje de días viene completo y las campañas traen su serie ALINEADA a ese eje (0 en los días sin gasto), para que el frontend no tenga que rellenar huecos. Los días los calcula Meta en la zona horaria de la cuenta publicitaria.\n","parameters":[{"in":"query","name":"days","schema":{"type":"integer","enum":[7,14,30,90]},"description":"Periodo en días (por omisión 30)"}],"responses":{"200":{"description":"Serie diaria por campaña"},"500":{"description":"Falta el token, no se pudo resolver la cuenta o Graph falló"}}}},"/api/admin/meta-diagnostico":{"get":{"summary":"Diagnóstico de la integración con Meta (solo lectura)","tags":["Leads / CRM"],"description":"Reporta el estado real de la integración según Graph: cuenta de Instagram vinculada a la página, campos de webhook a los que está suscrita la app y permisos del token relevantes para Instagram. No modifica nada.\n","responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/meta-forms/{meta_form_id}/agents":{"put":{"summary":"RETIRADO — la asignación de leads ahora es por PROYECTO","tags":["Leads / CRM"],"deprecated":true,"description":"La asignación por formulario se retiró (2026-07-24) y la que la sustituyó —el pool por anuncio y por campaña— también (2026-07-30, sus endpoints se borraron con las tablas meta_ad_agents / meta_campaign_agents ya sin uso). Hoy el reparto sale de la lista del PROYECTO: usar PUT /api/admin/mesa-rotacion-proyecto (pantalla Marketing → Configuración → Mesa de ayuda).\n","responses":{"410":{"description":"Endpoint retirado"}}}},"/api/admin/meta-forms":{"get":{"summary":"Listar formularios de Meta registrados (solo diagnóstico)","tags":["Leads / CRM"],"description":"Histórico/diagnóstico. Los formularios solo alimentan la precalificación (preguntas y pesos); la asignación de leads es POR ANUNCIO desde 2026-07-24 y este listado ya no tiene consumidores en la UI.\n","responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/amortization-table":{"get":{"summary":"Tabla de amortización","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/annual-amortization-table":{"get":{"summary":"Tabla de amortización anual","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/conversation":{"get":{"summary":"Conversación de WhatsApp del cliente de un financiamiento","description":"La conversación se guarda por NÚMERO, no por financiamiento: un chat de WhatsApp es con un teléfono. Aquí se resuelve el teléfono del cliente de ese financiamiento y se devuelven sus mensajes. Si el número está registrado con varias personas se avisa, porque entonces la conversación no es necesariamente solo de este cliente.\n","tags":["Mortgages"],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Financiamiento no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/conversation/send":{"post":{"summary":"Enviar un WhatsApp al cliente de este financiamiento","description":"Se envía DESDE la línea del proyecto de ese financiamiento. Escribir desde el financiamiento y no desde el cliente es a propósito: el financiamiento determina el proyecto y el proyecto determina la línea, así que nunca hay que adivinar por cuál número sale el mensaje.\n","tags":["Mortgages"],"responses":{"200":{"description":"Mensaje enviado"},"400":{"description":"Mensaje vacío o demasiado largo"},"404":{"description":"Financiamiento no encontrado"},"409":{"description":"Falta configuración (cliente sin teléfono o proyecto sin WhatsApp)"},"502":{"description":"El proveedor rechazó el envío"}}}},"/api/admin/mortgages/{mortgage_id}/notes":{"post":{"summary":"Agregar una nota interna al historial del financiamiento","description":"Las notas viven en notification_log con channel = 'nota', para que queden en la MISMA línea de tiempo que las notificaciones enviadas. Son internas: no se le mandan a nadie.\n","tags":["Mortgages"],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"201":{"description":"Nota agregada"},"400":{"description":"Falta el texto o el financiamiento es inválido"},"404":{"description":"Financiamiento no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/notification-log":{"get":{"summary":"Historial de notificaciones (email/WhatsApp) del financiamiento","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del financiamiento"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/payments":{"get":{"summary":"Obtener pagos por ID","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}":{"get":{"summary":"Obtener financiamientos por ID","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/mortgages/{mortgage_id}/send-statement":{"post":{"summary":"Enviar estado de cuenta","tags":["Admin - Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/owner-commissions/summary-by-project":{"get":{"summary":"Resumen de comisiones por proyecto","tags":["Admin - Comisiones Propietario"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/payments/{payment_id}/cancel":{"put":{"summary":"Cancelar","tags":["Admin - Pagos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/payments/{payment_id}":{"put":{"summary":"Actualizar pagos","tags":["Admin - Pagos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/poliza-comisiones":{"get":{"summary":"Póliza de comisiones","tags":["Admin - Pólizas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/poliza-efectivo":{"get":{"summary":"Póliza de efectivo","tags":["Admin - Pólizas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/project-media/{media_id}":{"put":{"summary":"Editar el título, el texto sugerido o el estado de un medio","tags":["Marketing / Galería"],"description":"NO permite cambiar el archivo (file_key) ni el proyecto. Cambiar el archivo dejaría vivos los identificadores que Meta ya devolvió para el archivo anterior, y los envíos seguirían mandando la foto vieja sin que nada lo delate: para reemplazar una foto se da de baja y se sube otra.\n","responses":{"200":{"description":"Medio actualizado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"404":{"description":"Medio no encontrado"}}},"delete":{"summary":"Dar de baja un medio de la galería","tags":["Marketing / Galería"],"description":"Baja LÓGICA (active = false). Ni la fila ni el objeto de S3 se borran: lead_events ya referenció este medio en conversaciones de clientes reales, y borrarlo dejaría huecos en historiales que sí se consultan. Es a propósito distinto de DELETE /api/files/{file_id}, que borra en duro.\n","responses":{"200":{"description":"Medio dado de baja"},"403":{"description":"Solo Administración o Marketing"},"404":{"description":"Medio no encontrado"}}}},"/api/admin/project-media/convert-audio":{"post":{"summary":"Convertir un audio subido en nota de voz y registrarlo","tags":["Marketing / Galería"],"description":"Tercer paso de la subida de un AUDIO, en lugar del registro normal. Recibe el key del archivo original que ya se subió a S3, lo convierte a ogg/opus mono con ffmpeg —el único formato que WhatsApp pinta como nota de voz— y registra el resultado. El original se borra: era desechable.\nEl bitrate NO es fijo: se calcula desde la duración para que el resultado caiga siempre debajo de los 512 KB en los que WhatsApp cambia el botón de play por uno de descarga.\nEl archivo no viaja en esta petición —se lee de S3— porque el cuerpo de una función de Vercel está topado en 4.5 MB y un audio lo rebasa.\n","responses":{"201":{"description":"Audio convertido y registrado"},"400":{"description":"Solicitud inválida o audio demasiado largo"},"403":{"description":"Solo Administración o Marketing"},"500":{"description":"Falló la conversión"}}}},"/api/admin/project-media/reorder":{"put":{"summary":"Reordenar la galería de un proyecto","tags":["Marketing / Galería"],"description":"Recibe los media_id en el orden deseado. El orden importa: la primera foto de la lista es la que más se va a mandar, porque es la que la agente ve primero cuando abre la galería en medio de una conversación.\n","responses":{"200":{"description":"Orden actualizado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"}}}},"/api/admin/project-media":{"get":{"summary":"Catálogo de medios de un proyecto (administración)","tags":["Marketing / Galería"],"description":"Devuelve TODOS los medios del proyecto, incluidos los dados de baja, porque esta es la pantalla que los administra. La bandeja consume /api/project-media, que solo entrega los activos.\n","parameters":[{"in":"query","name":"project_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Catálogo del proyecto"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"}}},"post":{"summary":"Registrar un medio ya subido a S3","tags":["Marketing / Galería"],"description":"Segundo paso de la subida: primero se pide el key con POST /api/admin/project-media/upload-url y se sube el archivo a S3 con esa URL, después se registra aquí. Valida mime y tamaño contra el mínimo común de los tres canales para que ningún medio del catálogo pueda ser rechazado después por Meta, frente a un cliente.\n","responses":{"201":{"description":"Medio registrado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"409":{"description":"Ese archivo ya estaba registrado"}}}},"/api/admin/project-media/upload-url":{"post":{"summary":"URL firmada para subir un medio de la galería a S3","tags":["Marketing / Galería"],"description":"Primer paso de la subida. A diferencia de /api/file-manager/generate-presigned-url —que firma la escritura de CUALQUIER key que le manden, sin pedir credenciales— aquí el key lo calcula el servidor y siempre cae bajo el prefijo project-media/. El navegador no elige dónde escribe.\n","responses":{"200":{"description":"URL de subida y key definitivo"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"}}}},"/api/admin/projects/{project_id}/mortgages":{"get":{"summary":"Obtener financiamientos por ID","tags":["Admin - Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/projects/commissions":{"get":{"summary":"Listar comisiones","tags":["Admin - Proyectos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/property/cron-update-reserved":{"get":{"summary":"Cron - actualizar reservaciones expiradas","tags":["Admin - Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/property":{"get":{"summary":"Listar propiedad","tags":["Admin - Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/property/status-available":{"get":{"summary":"Propiedades disponibles por estado","tags":["Admin - Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/property/update-status-v2":{"post":{"summary":"Actualizar estado de propiedad (v2)","tags":["Admin - Propiedades"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/property/update-status":{"post":{"summary":"Actualizar estado de propiedad","tags":["Admin - Propiedades"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/quick-replies/{quick_reply_id}":{"put":{"summary":"Editar una respuesta rápida (Administración o Marketing)","tags":["CRM"],"security":[{"bearerAuth":[]}],"description":"Actualiza SOLO los campos presentes en el body; lo que no se manda se queda como está. project_id null convierte la respuesta en GENERAL. Cuando status pasa a 'aprobada' se sella approved_by con el usuario que aprueba y approved_at con la fecha del servidor (así se aprueba o rechaza una propuesta hecha desde el chat).\n","parameters":[{"in":"path","name":"quick_reply_id","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"message_text":{"type":"string"},"project_id":{"type":"integer","nullable":true},"sort_order":{"type":"integer"},"active":{"type":"boolean"},"status":{"type":"string","enum":["aprobada","pendiente","rechazada"]},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Respuesta rápida actualizada"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"404":{"description":"Respuesta rápida no encontrada"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar una respuesta rápida (Administración o Marketing)","tags":["CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"quick_reply_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Respuesta rápida eliminada"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"404":{"description":"Respuesta rápida no encontrada"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/quick-replies/reorder":{"put":{"summary":"Reordenar respuestas rápidas (Administración o Marketing)","tags":["CRM"],"security":[{"bearerAuth":[]}],"description":"Recibe los quick_reply_id en el orden deseado y les asigna sort_order 1..N por posición, en UNA sola sentencia. Validación estricta del arreglo: si trae algo que no sea un entero positivo, ids repetidos o ids que no existen, responde 400 SIN tocar nada.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"integer"}},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Orden actualizado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/quick-replies":{"get":{"summary":"Catálogo COMPLETO de respuestas rápidas (administración)","tags":["CRM"],"security":[{"bearerAuth":[]}],"description":"Incluye inactivas, pendientes y rechazadas (a diferencia de /api/messenger-quick-replies, que solo devuelve las aprobadas y activas que consume el chat). Cada fila trae el proyecto (project_id null = respuesta GENERAL, sirve para todos los proyectos), quién la propuso y quién la aprobó. Orden: pendientes primero (son las que esperan revisión), luego por proyecto —generales primero— y sort_order.\n","responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear una respuesta rápida (Administración o Marketing)","tags":["CRM"],"security":[{"bearerAuth":[]}],"description":"Nace ya APROBADA (la crea quien administra el catálogo). Si no se envía sort_order se coloca al final de su grupo. project_id null = respuesta general. El texto puede traer {nombre} y {proyecto}: el chat los sustituye al insertar el mensaje.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","message_text","project_id","user_id"],"properties":{"label":{"type":"string","description":"Texto del chip (1 a 100 caracteres)"},"message_text":{"type":"string","description":"Mensaje que se inserta en el chat (1 a 2000 caracteres)"},"project_id":{"type":"integer","nullable":true,"description":"null = respuesta general (todos los proyectos)"},"sort_order":{"type":"integer"},"active":{"type":"boolean"},"user_id":{"type":"integer"}}}}}},"responses":{"201":{"description":"Respuesta rápida creada"},"400":{"description":"Solicitud inválida"},"403":{"description":"Solo Administración o Marketing"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/reports/cash-income":{"get":{"summary":"Ingresos en efectivo","tags":["Admin - Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/reports/sales-report":{"get":{"summary":"Reporte de ventas","tags":["Admin - Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/surveys":{"get":{"summary":"Listar encuestas","tags":["Admin - Encuestas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear encuestas","tags":["Admin - Encuestas"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/user-page-access":{"get":{"summary":"Permisos por página (marca) de los usuarios del staff","tags":["Leads / CRM"],"description":"Qué usuario ve y contesta qué páginas de Facebook (marcas) en la bandeja de Conversaciones y en la app móvil. Devuelve result.usuarios (usuarios ACTIVOS del staff, roles 1/3/15/17, cada uno con nombre, roles legibles y sus paginas de user_page_access) y result.paginas (el catálogo meta_page activo para pintar los checkboxes) — la forma EXACTA que consume la pestaña \"Accesos por marca\" de Marketing → Configuración. DEFAULT CERRADO (decisión del dueño 2026-08-19): un usuario sin renglones no ve NINGUNA conversación — el equipo actual quedó sembrado con ambas páginas al desplegar, así que aquí nadie aparece vacío por accidente. Requiere x-user-id con rol Administrador (3), Configuracion (10) o Marketing (15).\n","responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autenticado"},"403":{"description":"El usuario no puede administrar el acceso por página"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Reemplazar las páginas (marcas) a las que un usuario tiene acceso","tags":["Leads / CRM"],"description":"Reemplaza COMPLETOS los renglones de user_page_access de ESE usuario (las páginas que no vengan en page_ids se le quitan; page_ids vacío lo deja sin acceso a ninguna marca — default cerrado, es un estado válido). El reemplazo va en UN solo statement (DELETE + INSERT con CTEs, mismo patrón que mesa-rotacion-proyecto): el pool es compartido y un BEGIN/COMMIT multi-petición no es seguro; un statement es de por sí una transacción. Requiere x-user-id con rol Administrador (3), Configuracion (10) o Marketing (15).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","page_ids"],"properties":{"user_id":{"type":"integer"},"page_ids":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Permisos reemplazados"},"400":{"description":"Solicitud inválida (user_id o page_ids mal formados o inexistentes)"},"401":{"description":"No autenticado"},"403":{"description":"El usuario no puede administrar el acceso por página"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/users/{user_id}/conversation":{"get":{"summary":"Conversación de WhatsApp de un cliente","description":"Misma conversación que muestra el financiamiento, pero pedida por cliente. Se guarda por NÚMERO, así que si el teléfono está registrado con varias personas se avisa: la conversación no es necesariamente solo de este cliente.\n","tags":["Users"],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Usuario no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/admin/users/{user_id}/notification-log":{"get":{"summary":"Historial de notificaciones (email/WhatsApp) de todos los financiamientos del usuario como cliente","tags":["Admin - Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del usuario (cliente)"}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/agent-availability":{"get":{"summary":"Horarios de recorrido de cada promotor (plantilla y por fecha)","tags":["Leads / CRM"],"description":"Por omisión devuelve a TODOS los promotores activos (rol 1), INCLUIDOS los que no tienen ni un horario capturado: las pantallas de captura los necesitan en la lista justamente para poder capturárselos. Un promotor sin horarios llega con 'slots' vacío, no se omite.\nCon 'con_horarios=1' se omiten los que no tienen NADA que ofrecer. Ese filtro lo mandan solo las pantallas donde se PROPONE una visita (la hoja de la app de la Mesa y la bandeja web), donde un promotor sin horarios es ruido que hace elegir mal. Las de captura NO lo mandan.\nCada promotor trae 'tiene_horarios' (booleano), se haya filtrado o no. Cuenta la plantilla Y las capturas por fecha de hoy en adelante, y NO cuenta el centinela del día cerrado; por eso no se puede deducir de 'slots', que es solo la plantilla: quien trabaja únicamente por fechas concretas llega con 'slots' vacío y 'tiene_horarios' en true. Es una aproximación a propósito: no mira las citas ya apartadas ni la anticipación mínima, así que alguien con la agenda llena sigue apareciendo — eso lo dice /api/visit-slots, que sí lo sabe.\n'slots' es la PLANTILLA semanal recurrente (specific_date NULL), igual que siempre. Si además se mandan 'desde' y 'hasta', cada promotor trae 'dias' con la semana YA RESUELTA: para cada fecha real dice si lo que aplica viene de su captura de ESE día ('origen':'fecha', en firme) o se hereda de la plantilla ('origen':'plantilla'), cuántas visitas de una hora genera cada rango y cuántos minutos sobran sin usar. Un día capturado a propósito como \"no doy visitas\" llega con 'cerrado':true y 'rangos' vacío.\nLa resolución es por fecha LOCAL del negocio (America/Mazatlan), la misma que usa el generador de huecos: 'hoy' viene en la respuesta para que la pantalla marque el pasado sin consultar el reloj del navegador.\nEl nombre completo se arma y se limpia en SQL porque en users hay nombres con espacios de sobra ('Dulce Berenice ') que si no se normalizan hacen que el orden alfabético y el pintado se vean rotos.\n","parameters":[{"in":"query","name":"user_id","required":false,"schema":{"type":"integer"},"description":"Limita la respuesta a ese promotor (mismo formato)."},{"in":"query","name":"con_horarios","required":false,"schema":{"type":"string","enum":["1","true","yes","si","0","false","no"]},"description":"'1' deja solo a los promotores con horarios capturados (plantilla o fechas de hoy en adelante, sin contar los días cerrados) y devuelve 'con_horarios': true junto al resultado, para que quien pinte una lista vacía sepa que fue por el filtro y pueda decir dónde se capturan los horarios. Sin el parámetro salen todos. Un valor que no esté en la lista se responde con 400 en vez de tomarse como \"no\".\n"},{"in":"query","name":"desde","required":false,"schema":{"type":"string"},"description":"Primer día de la semana a resolver (AAAA-MM-DD). Va junto con 'hasta'."},{"in":"query","name":"hasta","required":false,"schema":{"type":"string"},"description":"Último día, inclusive. Máximo 62 días de rango."}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Parámetros inválidos"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Reemplaza la plantilla semanal O la captura de fechas concretas","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"description":"DOS MODOS EXCLUYENTES, uno por cuerpo:\n1) { user_id, slots:[{day_of_week,start_time,end_time}] } sustituye por completo la PLANTILLA semanal del agente y no toca ni sus fechas ni la plantilla de nadie más. Mandar 'slots' vacío es válido y significa \"este promotor ya no tiene plantilla\".\n2) { user_id, fechas:{ \"AAAA-MM-DD\": valor } } reemplaza la captura de CADA fecha listada (todo o nada) y no toca las demás fechas ni la plantilla. El valor puede ser un arreglo de rangos, [] o null para BORRAR la captura de ese día (vuelve a heredar la plantilla), { \"cerrado\": true } para dejarlo capturado como \"ese día no doy visitas\", o { \"rangos\": [...] }.\nCada rango es un RANGO ('de 17:15 a 18:30'), y de él salen tantas citas de una hora como quepan completas: 09:00–14:00 genera cinco (9, 10, 11, 12 y 13) y 17:00–17:45 no genera ninguna. Dos rangos del mismo día —del mismo day_of_week en la plantilla, o de la misma FECHA— no se pueden encimar: se responde 400 y no se guarda nada.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"El usuario no puede configurar horarios de recorridos"},"409":{"description":"El horario choca con otro renglón ya guardado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/clients":{"get":{"summary":"Obtener clients por ID","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/commissions/{commission_id}":{"get":{"summary":"Obtener comisiones por ID","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/commissions/{commission_id}/status":{"put":{"summary":"Actualizar estado","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Obtener estado","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/contracts":{"get":{"summary":"Obtener contratos por ID","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/customers":{"get":{"summary":"Obtener clientes por ID","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/daily-survey":{"get":{"summary":"Obtener encuesta diaria","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Enviar encuesta diaria","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar encuesta diaria","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/earnings":{"get":{"summary":"Obtener ganancias del agente","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/leads":{"get":{"summary":"Obtener leads por ID","tags":["Agentes"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/{agent_id}/signature-proposals":{"get":{"summary":"Obtener contratos en estatus Nuevo del agente con su propuesta de fecha de firma más reciente","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"agent_id","required":true,"schema":{"type":"integer"},"description":"ID del agente"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/agents/promoters-list":{"get":{"summary":"Lista de promotores","tags":["Agentes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bank-movements":{"get":{"summary":"Listar appbank-movements","tags":["Movimientos Bancarios"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appbank-movements","tags":["Movimientos Bancarios"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bank-movements/sync-bancox":{"post":{"summary":"Sincronizar con BancoX","tags":["Movimientos Bancarios"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Estado de sincronización","tags":["Movimientos Bancarios"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bank-reconciliation/auto-match":{"get":{"summary":"Obtener auto-coincidencias","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Ejecutar auto-conciliación","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bank-reconciliation":{"get":{"summary":"Listar appbank-reconciliation","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appbank-reconciliation","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appbank-reconciliation","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bank-reconciliation/stats":{"get":{"summary":"Listar stats","tags":["Conciliación Bancaria"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/bulk-payments/{bulk_payment_id}":{"get":{"summary":"Obtener appbulk-payments por ID","tags":["Pagos Masivos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"bulk_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/businesses/{business_id}":{"get":{"summary":"Obtener negocio por ID con contadores de cupones","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"business_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Negocio no encontrado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar negocio (nombre, prefijo, usuario vinculado) — solo administrador","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"business_id","required":true,"schema":{"type":"integer"},"description":"ID del negocio"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"business_name":{"type":"string"},"folio_prefix":{"type":"string"},"user_id":{"type":"integer","nullable":true}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"404":{"description":"Negocio no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/businesses":{"get":{"summary":"Listar negocios donde se redimen cupones","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"user_id","schema":{"type":"integer"},"description":"Filtrar por usuario de plataforma vinculado al negocio"}],"responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear negocio (solo administrador)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"business_name":{"type":"string"},"folio_prefix":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/chat/conversations/{lead_id}/follow-up":{"post":{"summary":"Agendar o quitar el próximo paso de una conversación","tags":["Leads / CRM"],"description":"Con action 'set' agenda la fecha en la que hay que volver a escribirle al cliente (follow_up_at) y una nota opcional del porqué. Con action 'clear' quita el pendiente. Ambas acciones quedan en lead_events para que el historial del lead cuente la misma historia que la bandeja. IMPORTANTE: la verdad del próximo paso es la tabla `seguimiento`, y follow_up_at es solo su espejo mientras la web no migre (fase 3). Por eso 'set' crea o corrige el seguimiento pendiente del lead (actividad 'escribir') y 'clear' lo cierra con resultado 'retirado'; la respuesta devuelve `seguimiento_id`, que viene null si el lead no tenía pendiente o si el entorno todavía no aplicó la migración.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"lead_id numérico o uuid del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["set","clear"]},"user_id":{"type":"integer"},"follow_up_at":{"type":"string","description":"ISO 8601. Requerido con action 'set'"},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/chat/conversations/{lead_id}/read":{"post":{"summary":"Marcar conversación como leída para un usuario","tags":["Leads / CRM"],"description":"Upsert en chat_read_state del último event_id leído por el usuario. Nunca retrocede el puntero (GREATEST), así que llamadas fuera de orden son seguras. Lo usa la bandeja de Conversaciones al abrir un chat.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"UUID o ID entero del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","last_read_event_id"],"properties":{"user_id":{"type":"integer"},"last_read_event_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/chat/conversations/{lead_id}/state":{"post":{"summary":"Resolver, reabrir, eliminar o restaurar una conversación de la bandeja del CRM","tags":["Leads / CRM"],"description":"El estado es global (no por usuario). Una conversación resuelta se reabre sola cuando el cliente vuelve a escribir (la bandeja compara resolved_at contra el último mensaje entrante); 'reopen' la reabre manualmente antes de eso. 'delete' oculta la conversación de la bandeja (soft delete con deleted_at/deleted_by); igual que la resolución, se revive sola si el cliente vuelve a escribir. 'restore' limpia el soft delete. Ninguna de las dos toca resolved_at.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"lead_id numérico o uuid del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["resolve","reopen","delete","restore"]},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/chat/conversations":{"get":{"summary":"Bandeja de Conversaciones del CRM (leads con chat)","tags":["Leads / CRM"],"description":"Una fila por lead activo que tenga eventos de chat (Messenger/WhatsApp/Instagram), con el último mensaje, total de mensajes, contador de no-leídos por usuario (chat_read_state) y la fecha del último mensaje entrante (para la ventana de 24h de Messenger). Ordenada por último mensaje. Las conversaciones eliminadas (soft delete vía deleted_at sin mensaje entrante posterior) se excluyen de la respuesta. Devuelve además el SEGUIMIENTO de la conversación (follow_up_at, follow_up_note y follow_up_pending, este último derivado: sigue pendiente mientras no haya un saliente real posterior a la fecha agendada), el turno en waiting_on ('nosotros' si el cliente escribió y nadie le ha contestado, 'cliente' si ya le respondimos) y client_phone para el botón de llamar cuando la ventana de 24h ya cerró. Multipágina: cada conversación trae page_id (página de Facebook dueña de la conversación) y es_pagina_crm (NULL cuenta como página del CRM, mismo fail-open que el panel de Campañas); el deep link a Business Suite debe armarse con el page_id DE LA FILA, no con el page_id de nivel respuesta (que es la página del CRM y queda por compatibilidad). PERMISOS POR PÁGINA (2026-08-19): la bandeja solo devuelve las conversaciones de las páginas a las que el user_id tiene acceso en user_page_access, con DEFAULT CERRADO — un usuario sin renglones recibe la lista vacía (decisión del dueño; el equipo actual quedó sembrado con ambas páginas, así que una bandeja vacía significa que a ese usuario nadie le ha dado acceso, no un error).\n","parameters":[{"in":"query","name":"user_id","required":true,"schema":{"type":"integer"},"description":"Usuario que mira la bandeja; define los no-leídos"},{"in":"query","name":"agent_id","required":false,"schema":{"type":"integer"},"description":"Si viene, la bandeja se limita a los leads asignados a ese agente (vista del promotor). Sin él devuelve todas las conversaciones.\n"}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"user_id requerido y numérico"},"500":{"description":"Error interno del servidor"}}}},"/api/cierres/contracts/{contract_id}/confirm-payment":{"post":{"summary":"Confirmar pago","tags":["Cierres"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cierres/contracts":{"get":{"summary":"Listar contratos","tags":["Cierres"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cierres/contracts/status-count":{"get":{"summary":"Conteo por estado","tags":["Cierres"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cobranza/overdue-contracts":{"get":{"summary":"Contratos con financiamiento vencido más de 90 días (3+ meses)","tags":["Cobranza"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/cancellation-letter":{"get":{"summary":"Generar carta de cancelación","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/change-agent":{"put":{"summary":"Cambiar agente","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/change-closing-agent":{"put":{"summary":"Cambiar agente de cierre","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/commissions":{"get":{"summary":"Obtener comisiones por ID","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/digital-signature":{"put":{"summary":"Registrar firma digital","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/mortgages":{"get":{"summary":"Obtener financiamientos por ID","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/notary-signature":{"put":{"summary":"Registrar firma notarial","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}":{"get":{"summary":"Obtener appcontracts por ID","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appcontracts","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"patch":{"summary":"Actualizar parcialmente appcontracts","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/status-updates/{status_update_id}":{"put":{"summary":"Actualizar status-updates","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"status_update_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/status-updates":{"get":{"summary":"Obtener status-updates por ID","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear status-updates","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/{contract_id}/transfer":{"get":{"summary":"Obtener traspaso","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Realizar traspaso","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Cancelar traspaso","tags":["Contratos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/available-for-transfer":{"get":{"summary":"Contratos disponibles para traspaso","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/generate-ratification":{"post":{"summary":"Generar ratificación","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts":{"get":{"summary":"Listar appcontracts","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appcontracts","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appcontracts","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/sales-report":{"get":{"summary":"Reporte de ventas","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/signature-date/digital":{"get":{"summary":"Obtener firmas digitales","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar firma digital","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/signature-date/notary":{"get":{"summary":"Obtener firmas notariales","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar firma notarial","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/signatures":{"get":{"summary":"Listado de firmas","tags":["Contratos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/contracts/update-status":{"post":{"summary":"Actualizar estado de propiedad","tags":["Contratos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/coupons/{folio}/redeem":{"post":{"summary":"Redimir cupón (usuario del negocio o administrador)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"folio","required":true,"schema":{"type":"string"},"description":"Folio del cupón (ej. LT-7XK4N2)"}],"responses":{"200":{"description":"Cupón redimido exitosamente"},"401":{"description":"No autorizado"},"403":{"description":"El cupón pertenece a otro negocio"},"404":{"description":"Cupón no encontrado"},"409":{"description":"El cupón ya no es redimible (redimido, cancelado o vencido)"},"500":{"description":"Error interno del servidor"}}}},"/api/coupons/{folio}":{"get":{"summary":"Verificar cupón por folio (muestra datos del cliente para validar identidad)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"folio","required":true,"schema":{"type":"string"},"description":"Folio del cupón (ej. LT-7XK4N2)"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"403":{"description":"El cupón pertenece a otro negocio"},"404":{"description":"Cupón no encontrado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Cancelar cupón (solo administrador)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"folio","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"404":{"description":"Cupón no encontrado"},"409":{"description":"El cupón no se puede cancelar en su estado actual"},"500":{"description":"Error interno del servidor"}}}},"/api/coupons":{"get":{"summary":"Listar cupones (filtros por negocio, cliente y estado)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"business_id","schema":{"type":"integer"}},{"in":"query","name":"customer_id","schema":{"type":"integer"}},{"in":"query","name":"status","schema":{"type":"string"},"description":"ACTIVE | REDEEMED | CANCELLED"}],"responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear cupón para un cliente (solo administrador)","tags":["Cupones"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","business_id","amount"],"properties":{"customer_id":{"type":"integer"},"business_id":{"type":"integer"},"amount":{"type":"number","example":600},"expires_at":{"type":"string","format":"date","nullable":true},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/apartado-reminders":{"get":{"summary":"Listar apartado-reminders","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/cobranza-report":{"get":{"summary":"Cron - reporte de cobranza","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/commission-payment-reminders":{"get":{"summary":"Cron - recordatorios de pago de comisiones","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/mesa-ayuda":{"get":{"summary":"Cron - escalamiento de la Mesa de ayuda","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"description":"Corre cada minuto en horario de atención (por omisión 06:00–23:59 America/Mazatlan; el horario, los minutos de espera y la tolerancia de apertura se configuran en mesa_ayuda_settings). SOLO PRIMER CONTACTO: busca leads de la Mesa de ayuda que NUNCA han recibido una respuesta real (sin mensajes salientes; las notas y la auto-respuesta fuera de horario no cuentan) y llevan más de N minutos esperando (5 por omisión), y los escala a la siguiente posición de LA LISTA DE SU PROYECTO (mesa_rotation_project; los leads sin proyecto usan la lista GENERAL) con aviso por WhatsApp. El reloj de espera arranca en el último mensaje del cliente si hubo chat, y en leads.created_at si el lead llegó por FORMULARIO y nunca escribió (o desde el último escalón, lo que sea más reciente). En cuanto el cliente recibe cualquier respuesta real, el lead se queda con su agente y la mesa no lo vuelve a tocar. El lead de FORMULARIO además solo escala si nació DESPUÉS del corte de despliegue (MESA_FORMULARIO_ESCALA_DESDE), si viene de Meta Lead Ads y si NADIE lo ha tocado (sigue en 'Nuevo Lead', sin last_contact_date y sin llamadas/visitas/notas): esos leads se trabajan por teléfono y ahí no hay mensaje de chat que apague el reloj. Si ya está en la última posición (mesa agotada), el lead REGRESA a la posición 1 de esa misma lista, se registra 'mesa_alert' (no se re-escala hasta que el cliente vuelva a escribir) y se alerta al usuario 1. Durante los primeros `tolerancia_apertura_min` minutos después de abrir NO se escala nada (0 = como siempre), para que el rezago de la madrugada no queme la lista completa a la hora de abrir. RED DE SEGURIDAD: además del escalamiento, cada corrida busca leads que se CAYERON de su lista —su agente ya no pertenece a la lista efectiva del lead, por ejemplo porque leads.project_id cambió al vincular una propiedad— y los devuelve al primer usuario habilitado de esa lista, con evento 'assigned', 'mesa_alert' (una sola vez) y avisos. Solo entra ahí el lead cuya ÚLTIMA asignación fue de la mesa: nunca se le quita un lead a un promotor. Con ?dry_run=1 solo reporta qué haría (incluyendo de qué lista salió cada decisión, qué leads rescataría, la configuración vigente y si estaría en tolerancia de apertura), sin escribir en BD ni enviar mensajes.\n","parameters":[{"in":"query","name":"dry_run","required":false,"schema":{"type":"string"},"description":"Con valor 1: análisis sin efectos (ni BD ni WhatsApp)"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/monthly-summary":{"get":{"summary":"Resumen mensual","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/openpay-subscription-check":{"get":{"summary":"Cron - verificar suscripciones OpenPay","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/payment-reminders":{"get":{"summary":"Cron - recordatorios de pago","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/reservation-reminders":{"get":{"summary":"Cron - recordatorios de reservación","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/seguimientos":{"get":{"summary":"Cron - recordatorios de seguimientos (ARRANCA DORMIDO)","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"description":"Dos avisos sobre la tabla `seguimiento`: (1) A LA HORA, cuando a un seguimiento pendiente le llegó su `agendado_para` y todavía no se avisa (`avisado_en IS NULL`); se marca `avisado_en` para no repetirlo. (2) RESUMEN MATUTINO, una sola vez al día entre las 8 y las 9 de America/Mazatlan, a cada persona con seguimientos de hoy o atrasados. NACE APAGADO: sin la variable de entorno SEGUIMIENTOS_AVISOS='activo' corre en MODO SIMULACRO — calcula todo y devuelve a quién le habría escrito y con qué texto, pero no manda un solo mensaje ni marca nada. Con ?dry_run=1 el análisis sin efectos se puede pedir aunque los avisos ya estén activos, y con ?resumen=1 se evalúa el resumen matutino fuera de su banda horaria (para poder verificarlo a cualquier hora).\n","parameters":[{"in":"query","name":"dry_run","required":false,"schema":{"type":"string"},"description":"Con valor 1: análisis sin efectos (ni BD ni mensajes)"},{"in":"query","name":"resumen","required":false,"schema":{"type":"string"},"description":"Con valor 1: evalúa el resumen matutino aunque no sea su hora"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/stripe-subscription-check":{"get":{"summary":"Cron - vigilancia de domiciliaciones Stripe","description":"Resincroniza contra Stripe las suscripciones vigentes del entorno actual (test o live), detecta las que quedaron en past_due / unpaid / incomplete y las tarjetas que vencen dentro de los proximos 60 dias, y manda una sola alerta de WhatsApp a COBRANZA_ADMIN_PHONE.\n","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"revisadas":{"type":"integer"},"con_problema":{"type":"integer"},"tarjetas_por_vencer":{"type":"integer"},"alerta_enviada":{"type":"boolean"}}}}}},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/ventana-24h":{"get":{"summary":"Cron - aviso antes de que cierre la ventana de 24h de Meta","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"description":"Corre cada 15 minutos en horario de atención (06:00–23:59 America/Mazatlan). Avisa por WhatsApp al AGENTE ASIGNADO cuando a una conversación le quedan menos de HORAS_AVISO horas de ventana de 24h y todavía tiene algo pendiente: o el cliente escribió y nadie le contestó, o hay un seguimiento agendado que sigue sin cumplirse. Es el último momento en que se le puede escribir SIN COSTO. Se avisa UNA sola vez por ventana (window_alert_at); si el cliente vuelve a escribir, la ventana es nueva y el aviso se rehabilita solo. Con ?dry_run=1 solo reporta a quién avisaría, sin escribir en BD ni mandar WhatsApp.\n","parameters":[{"in":"query","name":"dry_run","required":false,"schema":{"type":"string"},"description":"Con valor 1: análisis sin efectos (ni BD ni WhatsApp)"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/cron/visitas-disponibilidad":{"get":{"summary":"Cron - promotores sin un solo horario de visita la semana entrante","tags":["Cron Jobs"],"security":[{"bearerAuth":[]}],"description":"Corre los VIERNES y manda UN WhatsApp a los usuarios activos con rol Marketing ('15') con la lista de promotores que la semana entrante (lunes a domingo siguientes) NO tendrían NI UN horario disponible para dar recorridos. Si todos tienen aunque sea uno, NO manda nada.\nEL AVISO ES POR RESULTADO, no por captura: no dice \"no capturaste\", dice \"a esta persona no se le puede proponer una sola visita\". Así cubre igual al que no capturó y al que capturó mal (rangos de 40 minutos, un día cerrado por error, la semana equivocada), y no molesta a quien tiene plantilla base que ya cubre la semana.\nCon ?dry_run=1 analiza y devuelve la foto completa —los huecos de cada promotor, quiénes quedarían en cero y por qué canal saldría cada aviso— SIN escribir nada ni mandar un solo mensaje.\n","parameters":[{"in":"query","name":"dry_run","required":false,"schema":{"type":"string"},"description":"Con valor 1: análisis sin efectos (no manda WhatsApp)"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/devices/register":{"post":{"summary":"Registrar un teléfono para recibir avisos push","tags":["Notificaciones"],"description":"Guarda el token de Firebase de un dispositivo a nombre del usuario de la SESIÓN (el user_id sale del JWT, no del cuerpo). Si el token ya existía a nombre de otra persona, cambia de dueño: el token identifica al teléfono, no al usuario.\n","responses":{"200":{"description":"Dispositivo registrado"},"400":{"description":"Falta el token"},"401":{"description":"Sin sesión válida"}}}},"/api/devices/unregister":{"post":{"summary":"Dejar de recibir avisos push en un teléfono","tags":["Notificaciones"],"description":"Da de baja el token. No pide sesión a propósito: se llama al CERRAR sesión, cuando la cookie ya puede haber expirado, y lo único que hace es apagar avisos de un token que quien llama ya conoce. El daño posible es dejar de recibir avisos, nunca recibir los de otra persona — al revés que el registro, que sí exige sesión.\n","responses":{"200":{"description":"Dispositivo dado de baja (o ya lo estaba)"}}}},"/api/file-manager/generate-download-url":{"post":{"summary":"Generar URL de descarga","tags":["Archivos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/file-manager/generate-presigned-url":{"post":{"summary":"Generar URL pre-firmada para subir archivo","tags":["Archivos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/files/{file_id}":{"get":{"summary":"Obtener appfiles por ID","tags":["Archivos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"file_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appfiles","tags":["Archivos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"file_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appfiles","tags":["Archivos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"file_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/files":{"get":{"summary":"Listar appfiles","tags":["Archivos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appfiles","tags":["Archivos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/financial-transactions":{"get":{"summary":"Listar appfinancial-transactions","tags":["Transacciones Financieras"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appfinancial-transactions","tags":["Transacciones Financieras"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/identification-types":{"get":{"summary":"Listar appidentification-types","tags":["Catálogos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/job-applications":{"post":{"summary":"Crear appjob-applications","tags":["Solicitudes de Empleo"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Listar appjob-applications","tags":["Solicitudes de Empleo"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/landing_pages/projects/{project_uuid}":{"get":{"summary":"Obtener información de un proyecto por UUID","tags":["Projects"],"parameters":[{"in":"path","name":"project_uuid","required":true,"schema":{"type":"string"},"description":"UUID único del proyecto"}],"responses":{"200":{"description":"Proyecto encontrado exitosamente","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"object","properties":{"project_id":{"type":"integer"},"project_name":{"type":"string"},"project_description":{"type":"string"},"category":{"type":"string"},"owner_id":{"type":"integer"},"owner_full_name":{"type":"string"},"owner_email":{"type":"string"},"project_status_name":{"type":"string"}}}}}}}},"404":{"description":"Proyecto no encontrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Error interno del servidor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/lead-sources":{"get":{"summary":"Listar applead-sources","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/activities":{"post":{"summary":"Crear actividades","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Obtener actividades por ID","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/assign":{"put":{"summary":"Asignar un lead a un agente","tags":["Leads / CRM"],"description":"Reasigna el lead al agente indicado, registra el evento en el historial y notifica por WhatsApp al agente que tiene un nuevo lead en su CRM.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"ID o UUID del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"integer"},"user_id":{"type":"integer","description":"Usuario que hace la asignación (para el historial)"}}}}}},"responses":{"200":{"description":"Lead asignado"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead o agente no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/chat-messages":{"get":{"summary":"Conversación del lead (Messenger, WhatsApp e Instagram)","tags":["Leads / CRM"],"description":"Mensajes del chat del lead en orden cronológico, leídos del historial de eventos (event_category messenger/whatsapp/instagram). Soporta after_id para sondeo incremental desde la UI.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"after_id","required":false,"schema":{"type":"integer"},"description":"Devolver solo mensajes con event_id mayor a este valor"}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/chat-note":{"post":{"summary":"Nota interna en el chat del lead (el cliente NO la ve)","tags":["Leads / CRM"],"description":"Registra una nota interna dentro de la línea de tiempo del chat (direction 'note'). No envía nada al cliente, no cuenta como no-leído y no afecta la ventana de 24h. Disponible aunque la ventana esté cerrada o el canal aún no soporte envío.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Nota registrada"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/customer-contact":{"put":{"summary":"Actualizar teléfono y/o email del cliente vinculado al lead","tags":["Leads / CRM"],"description":"Actualiza SOLO los datos de contacto (phone_number y/o email) del usuario cliente vinculado al lead (leads.customer_id → users). Debe venir al menos una de las dos claves; enviar null o cadena vacía limpia ese campo. Registra el cambio en el historial del lead (lead_events).\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"ID o UUID del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id"],"properties":{"phone_number":{"type":"string","nullable":true,"description":"Nuevo teléfono (null o \"\" para limpiar). Se guarda tal cual viene (trim)."},"email":{"type":"string","nullable":true,"description":"Nuevo email (null o \"\" para limpiar)"},"user_id":{"type":"integer","description":"Usuario que hace el cambio (para el historial)"}}}}}},"responses":{"200":{"description":"Contacto actualizado","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"customer":{"type":"object","properties":{"phone_number":{"type":"string","nullable":true},"email":{"type":"string","nullable":true}}}}}}}},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/instagram-reply":{"post":{"summary":"Responder por Instagram (DM) a un lead desde el CRM","tags":["Leads / CRM"],"description":"Envía un mensaje de texto al IGSID del lead vía la Send API de la página (Meta enruta al Instagram vinculado) y lo registra como actividad. Meta solo permite responder dentro de las 24 horas posteriores al último mensaje del cliente; fuera de la ventana responde 422 con window_expired.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Mensaje enviado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Sin acceso a la marca (página) de la conversación (sin_acceso_pagina), el lead está asignado a otra persona (turno_perdido), o Meta rechazó el envío por permisos — pages_messaging aún sin acceso avanzado (sin_permiso)\n"},"404":{"description":"Lead no encontrado"},"422":{"description":"Ventana de 24 horas vencida"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/messenger-reply":{"post":{"summary":"Responder por Messenger a un lead desde el CRM","tags":["Leads / CRM"],"description":"Envía un mensaje de texto al PSID del lead vía la Send API de la página y lo registra como actividad. Meta solo permite responder dentro de las 24 horas posteriores al último mensaje del cliente; fuera de la ventana responde 422 con window_expired.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Mensaje enviado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Sin acceso a la marca (página) de la conversación (sin_acceso_pagina), el lead está asignado a otra persona (turno_perdido), o Meta rechazó el envío por permisos — pages_messaging aún sin acceso avanzado (sin_permiso)\n"},"404":{"description":"Lead no encontrado"},"422":{"description":"Ventana de 24 horas vencida"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/properties":{"post":{"summary":"Crear propiedades","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Obtener propiedades por ID","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar propiedades","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}":{"get":{"summary":"Obtener appleads por ID","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appleads","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/seguimiento":{"get":{"summary":"Pendiente actual del lead y último seguimiento cerrado","tags":["Leads / CRM"],"description":"Devuelve { pendiente, ultimo_cerrado }. `pendiente` es el único seguimiento abierto del lead (el índice único parcial garantiza que no pueda haber dos) y viene con la marca `vencido` calculada con el reloj del servidor. `ultimo_cerrado` es el paso anterior de la cadena, para que la ficha pueda decir \"la vez pasada no contestó\".\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"lead_id numérico o uuid del lead"},{"in":"query","name":"user_id","required":false,"schema":{"type":"integer"},"description":"Respaldo de identidad mientras el middleware no inyecte x-user-id. Con identidad se valida el acceso a la marca del lead.\n"}],"responses":{"200":{"description":"Operación exitosa"},"403":{"description":"El usuario no tiene acceso a la marca del lead"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"},"503":{"description":"El módulo no está migrado en este entorno"}}},"post":{"summary":"Agendar el pendiente del lead, o registrar el desenlace de algo que ya pasó","tags":["Leads / CRM"],"description":"DOS USOS SEGÚN EL CUERPO. (a) SIN `resultado`: crea el seguimiento pendiente del lead y deja el renglón en el hilo del chat (event_type 'seguimiento', direction 'note'). Un lead solo puede tener UN pendiente: si ya lo tiene responde 409 con ya_existe y el pendiente que ganó, para que la app muestre ese en vez de duplicarlo. `agendado_para` y `actividad` son obligatorios y la respuesta es 201. (b) CON `resultado`: registra el desenlace de una conversación que YA ocurrió, sin haber agendado nada antes (el atajo del menú ⋮ del chat). El seguimiento nace CERRADO (agendado_para = ahora), `actividad` es opcional y vale 'otro' por omisión —es un registro de lo que pasó, no un plan—, y un `agendado_para` en el cuerpo se ignora. Si el lead YA tenía un pendiente vivo NO se crea un segundo renglón: se cierra ese con el resultado, y la respuesta lo avisa con `pendiente_cerrado` para que la app refresque. Si viene además `siguiente`, se encadena el próximo paso con su anterior_id, con las mismas reglas que el cierre normal. El resultado aplica LAS MISMAS consecuencias que PATCH /api/seguimientos/{id} con accion 'cerrar', porque las ejecuta la misma función compartida (app/utilis/seguimientoResultado.js): los cuatro resultados de pérdida mueven a Perdido SOLO si el lead no tiene propiedades y no viene de Reservación/Apartado/Postpuesto —si las tiene, el resultado se registra pero la etapa NO se mueve y la respuesta trae etapa_movida false con motivo y mensaje listos para mostrar—, y contesto / pidio_info mueven a Contacto Inicial solo mientras el lead siga en Nuevo. La respuesta tiene la MISMA FORMA que la del cierre normal (etapa_movida, etapa_nueva, motivo_no_movida, mensaje_no_movida, resultado_tipo, sugerencia_siguiente, siguiente, event_id...) y el mismo código 200, para que la app no necesite dos manejos.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id"],"properties":{"agendado_para":{"type":"string","description":"ISO 8601, futuro (5 min de gracia). Obligatorio al agendar; se ignora cuando viene `resultado`.\n"},"actividad":{"type":"string","enum":["llamar","escribir","informacion","visita","otro"],"description":"Obligatorio al agendar. Con `resultado` es opcional y vale 'otro' por omisión.\n"},"nota":{"type":"string"},"user_id":{"type":"integer"},"resultado":{"type":"string","description":"Registra el desenlace de algo que ya pasó. Mismo catálogo que el cierre normal.\n","enum":["contesto","no_contesto","pidio_info","reprogramar","quiere_visita","confirma_el","gano","desistio","fuera_de_perfil","competencia","numero_equivocado","retirado"]},"resultado_nota":{"type":"string","description":"Solo con `resultado`."},"siguiente":{"type":"object","description":"Solo con `resultado`: encadena el próximo paso. No se permite con los resultados que cierran el lead (los cuatro de pérdida y gano).\n","properties":{"agendado_para":{"type":"string"},"actividad":{"type":"string"},"nota":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Resultado registrado (cuerpo con `resultado`)"},"201":{"description":"Seguimiento creado"},"400":{"description":"Solicitud inválida"},"403":{"description":"El usuario no tiene acceso a la marca del lead"},"404":{"description":"Lead no encontrado"},"409":{"description":"El lead ya tiene un pendiente, o ya está cerrado"},"500":{"description":"Error interno del servidor"},"503":{"description":"El módulo no está migrado en este entorno"}}}},"/api/leads/{lead_id}/send-media":{"post":{"summary":"Mandarle al lead una foto o un video de la galería del proyecto","tags":["Leads / CRM"],"description":"Manda un medio del catálogo curado (project_media) por el canal de la conversación. Una sola ruta para los tres canales, y no un campo más en los tres *-reply, porque en Messenger e Instagram el adjunto y su texto son DOS llamadas al Send API en serie —no hay caption— y esa lógica no cabe en una ruta de texto sin ensuciar el camino que hoy funciona.\nFuera de la ventana de 24 horas responde 422: mandar una foto fuera de ventana requeriría una plantilla con header de media, que no existe.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"media_id":{"type":"integer"},"caption":{"type":"string"},"channel":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Medio enviado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Sin acceso a la marca (página) de la conversación (sin_acceso_pagina), o el lead está asignado a otra persona (turno_perdido)\n"},"404":{"description":"Lead o medio no encontrado"},"409":{"description":"Ese mismo medio se acaba de enviar"},"422":{"description":"Ventana de 24 horas vencida"},"502":{"description":"Meta rechazó el envío"}}}},"/api/leads/{lead_id}/status":{"put":{"summary":"Actualizar estado","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/sugerir-respuesta":{"post":{"summary":"Propone tres mensajes para la conversación (NO los envía)","tags":["Leads / CRM"],"description":"Lee el hilo, lo diagnostica con el método de la casa y devuelve tres propuestas de mensaje con su porqué, más el seguimiento sugerido. Dentro de la ventana de 24 h propone texto libre; fuera de ella propone cuál plantilla aprobada usar y con qué variables, porque es lo único que WhatsApp deja enviar. NUNCA envía nada. Si el anuncio del que nació el lead tiene ligada una propiedad de intermediación (meta_lead_ads.gb_property_code), consulta su ficha EN VIVO contra la API del anunciante y se la pasa al modelo, para que los huecos de precio y medidas salgan llenos. Si esa API falla o tarda, la sugerencia se genera igual, con huecos: la respuesta trae propiedad_externa.ficha_disponible para que el asesor lo sepa. Restringido a los usuarios de USUARIOS_CON_SUGERENCIA, y por la identidad REAL: sigue funcionando mientras el súper admin ve la app como otro usuario.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"lead_id o uuid del lead"}],"responses":{"200":{"description":"Sugerencias generadas"},"400":{"description":"Solicitud inválida"},"403":{"description":"El usuario no tiene habilitado el botón"},"404":{"description":"Lead no encontrado"},"502":{"description":"El modelo no devolvió algo utilizable"},"503":{"description":"Falta ANTHROPIC_API_KEY en el entorno"}}}},"/api/leads/{lead_id}/visit":{"post":{"summary":"Agendar la visita al proyecto de un lead","tags":["Leads / CRM"],"description":"Crea la cita de recorrido y la registra en la cronología del lead como actividad de categoría 'visit'. Por defecto nace en estado 'propuesta' (el cliente todavía no la confirma).\nCon confirmada=true (flujo nuevo 2026-08-13: el cliente YA aceptó el horario en el chat) la cita nace directamente en 'confirmada' y, en la misma transacción, el lead se mueve a la etapa Visita Programada con las mismas reglas que el PATCH de /api/visits/{appointment_id}: solo desde Nuevo Lead, Contacto Inicial, Postpuesto o sin etapa — a un lead en Reservación, Apartado, Ganado o Perdido no se le regresa la etapa. Tras confirmar, se avisa por WhatsApp al promotor que da el recorrido (aviso interno best-effort: si falla, la cita queda creada igual). El lead NO se asigna al promotor: la asignación sigue siendo manual.\nSi el lead ya tenía otra PROPUESTA viva, esa se cancela sola en la misma transacción (\"mejor el domingo\" no debe dejar el sábado del promotor bloqueado); las confirmadas no se tocan.\nEl horario es EXCLUSIVO del promotor: si ese mismo agente ya tiene una cita viva (propuesta o confirmada) a esa hora, responde 409 con ocupado=true para que quien está en el chat elija otro hueco.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"},"description":"lead_id entero o uuid del lead"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","scheduled_at"],"properties":{"user_id":{"type":"integer","description":"Quién DA el recorrido"},"scheduled_at":{"type":"string","description":"ISO 8601. Con zona ('2026-08-16T10:00:00-07:00') se toma tal cual; sin zona ('2026-08-16T10:00') se interpreta en America/Mazatlan.\n"},"proposed_by":{"type":"integer","description":"Quién la propuso desde el chat"},"project_id":{"type":"integer","description":"Si no viene, se hereda del lead"},"notes":{"type":"string"},"confirmada":{"type":"boolean","description":"true = la cita nace 'confirmada' (el cliente ya aceptó el horario en el chat), mueve el lead a Visita Programada y avisa al promotor por WhatsApp. Omitido o false, nace en 'propuesta' como siempre.\n"}}}}}},"responses":{"201":{"description":"Cita creada"},"400":{"description":"Solicitud inválida"},"404":{"description":"Lead no encontrado"},"409":{"description":"Ese horario ya está ocupado para ese promotor"},"500":{"description":"Error interno del servidor"},"503":{"description":"Falta aplicar la migración de visitas"}}},"get":{"summary":"Cita viva del lead","tags":["Leads / CRM"],"description":"Devuelve la cita en estado 'propuesta' o 'confirmada' más relevante del lead (la próxima futura y, si ya no hay futuras, la última que se pasó sin cerrar), o null si no tiene ninguna.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/leads/{lead_id}/whatsapp-reactivar":{"get":{"summary":"Estado de envío por plantilla y vista previa de las plantillas","tags":["Leads / CRM"],"description":"NO envía nada. Devuelve todo lo que necesita el modal del botón de WhatsApp: si el lead ya tiene conversación, si la ventana de 24 h está abierta, a qué destino se le escribiría (wa_id de Meta o teléfono del cliente), los candados que hoy bloquearían el envío (turno, plantilla reciente, tope diario, canal propio del cliente, intentos rechazados) y el catálogo de plantillas aprobadas con el texto YA sustituido. La vista previa se arma en el servidor con los MISMOS valores que usará el POST, para que lo que ve la persona sea exactamente lo que le llegará al cliente.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"user_id","required":false,"schema":{"type":"integer"},"description":"Usuario que abre el modal (para evaluar turno y tope). RESPALDO del header x-user-id que pone el middleware; el header manda sobre este valor"},{"in":"query","name":"purpose","required":false,"schema":{"type":"string"},"description":"Filtra el catálogo por propósito"}],"responses":{"200":{"description":"Estado de envío"},"404":{"description":"Lead no encontrado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Enviar una plantilla aprobada al cliente desde la línea de la empresa","tags":["Leads / CRM"],"description":"Envía UNA plantilla APROBADA del catálogo (whatsapp_template) al lead y la registra en su conversación. El destinatario es el wa_id del lead si ya lo tiene; si no —caso típico de los leads de formulario— el teléfono del cliente normalizado. El wa_id que Meta devuelve al enviar se guarda en el lead para que la respuesta del cliente no cree un lead duplicado. Candados en el servidor - solo el agente asignado (403), una plantilla por lead cada 24 h con candado por lead (409), tope diario por usuario (429), ventana de 24 h abierta (409, porque responder es gratis), cliente que solo existe en Messenger o Instagram (409) y tope de intentos rechazados por Meta (429).\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer","description":"Quién manda la plantilla. RESPALDO del header x-user-id que pone el middleware; el header manda sobre este valor. Requerido si no hay header: queda en la bitácora y limita el tope diario"},"template_id":{"type":"integer","description":"Plantilla exacta del catálogo. Si falta, se usa purpose"},"purpose":{"type":"string","description":"Propósito del catálogo. Default: reactivacion_general"}}}}}},"responses":{"200":{"description":"Plantilla enviada"},"400":{"description":"Falta user_id, o el cliente no tiene un teléfono usable"},"403":{"description":"El lead está asignado a otro agente"},"404":{"description":"Lead no encontrado"},"409":{"description":"Ventana abierta (responder es gratis), ya se le mandó una plantilla en 24 h, el cliente solo existe en Messenger/Instagram, o su conversación es de otra línea de WhatsApp (linea_ajena)"},"422":{"description":"La plantilla usa variables que el sistema no sabe llenar"},"429":{"description":"Tope diario de plantillas por usuario, o tope de intentos rechazados por Meta para este lead"},"500":{"description":"Error interno del servidor"},"502":{"description":"Meta rechazó el envío"},"503":{"description":"Plantilla no configurada o no aprobada"}}}},"/api/leads/{lead_id}/whatsapp-reply":{"post":{"summary":"Responder por WhatsApp (Cloud API) a un lead desde el CRM","tags":["Leads / CRM"],"description":"Envía un mensaje de texto al wa_id del lead vía la Cloud API y lo registra en su conversación. Meta solo permite texto libre dentro de las 24 horas posteriores al último mensaje del cliente; fuera de la ventana responde 422 con window_expired.\n","parameters":[{"in":"path","name":"lead_id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"user_id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Mensaje enviado"},"400":{"description":"Solicitud inválida"},"403":{"description":"Sin acceso a la marca (página) de la conversación (sin_acceso_pagina), o el lead está asignado a otra persona (turno_perdido)\n"},"404":{"description":"Lead no encontrado"},"422":{"description":"Ventana de 24 horas vencida"},"500":{"description":"Error interno del servidor"}}}},"/api/leads":{"post":{"summary":"Crear appleads","tags":["Leads / CRM"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/login":{"post":{"summary":"Crear applogin","tags":["Autenticación"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/logout":{"post":{"summary":"Crear applogout","tags":["Autenticación"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mailersend/webhook":{"post":{"summary":"Webhook de eventos de entrega de MailerSend","tags":["Notificaciones"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Evento procesado"},"401":{"description":"Firma inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/meeting-locations":{"get":{"summary":"Listar appmeeting-locations","tags":["Ubicaciones de Reunión"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mesa-ayuda/members":{"get":{"summary":"Miembros activos del rol Mesa de ayuda (17)","tags":["Leads / CRM"],"description":"Catálogo de usuarios con el rol Mesa de ayuda (17), en orden ALFABÉTICO. Cada fila trae 'nombre' (nombre completo), 'activo' y 'tiene_rol' como banderas booleanas.\nYA NO DEVUELVE 'position' NI ORDENA POR ELLA (2026-08-12): esa columna salía de mesa_ayuda_rotation, la rotación GLOBAL única que quedó muerta cuando el escalamiento pasó a ser POR PROYECTO. Ordenar por ella hacía que este listado presentara como vigente una cadena que ya no gobierna nada. El orden de atención de verdad vive en mesa_rotation_project y lo sirve GET /api/admin/mesa-rotacion-proyecto (Marketing → Configuración → Mesa de ayuda), que además ya trae su propio catálogo de miembros.\n","parameters":[{"in":"query","name":"incluir_bajas","required":false,"schema":{"type":"string"},"description":"Con valor 1 agrega TAMBIÉN a quien sigue apareciendo en la vieja mesa_ayuda_rotation pero ya se desactivó o perdió el rol 17 (llegan con activo o tiene_rol en false). Es lo ÚNICO que aún consulta esa tabla muerta y hoy no tiene consumidor; sin el parámetro el listado es solo miembros activos con el rol. OJO: con incluir_bajas=1 se requiere x-user-id de un usuario con rol Administrador (3), Configuracion (10) o Marketing (15), porque esa rama revela a gente que atendió leads reales.\n"}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autenticado (solo con incluir_bajas=1)"},"403":{"description":"El usuario no puede ver la Mesa de ayuda (solo con incluir_bajas=1)"},"500":{"description":"Error interno del servidor"}}}},"/api/messenger-quick-replies":{"get":{"summary":"Catálogo de respuestas rápidas del chat (generales + del proyecto)","tags":["CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"project_id","required":false,"schema":{"type":"integer"},"description":"Proyecto del lead. Con project_id se devuelven las respuestas GENERALES (project_id null) seguidas de las de ese proyecto; si esa combinación no arroja NINGUNA respuesta, se devuelve el catálogo completo (fallback transitorio, mientras no existan generales). Sin project_id (o inválido) se devuelven las generales seguidas de las de TODOS los proyectos, para no dejar el chat sin botones.\n"},{"in":"query","name":"page_id","required":false,"schema":{"type":"string"},"description":"Página de Facebook de la conversación (leads.page_id). Ausente o igual a la página del CRM: comportamiento histórico completo (el catálogo con page_id NULL es de la página del CRM). Con la página de OTRA marca se devuelven SOLO las respuestas con ese page_id (las generales NULL son textos de Mi Terreno y NO se heredan, regla de oro multipágina) y SIN el fallback del catálogo completo: si esa página no tiene respuestas capturadas, el resultado es vacío y el chat muestra cero chips, que es lo correcto.\n"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"array","items":{"type":"object","properties":{"quick_reply_id":{"type":"integer"},"project_id":{"type":"integer","nullable":true},"project_name":{"type":"string","nullable":true},"page_id":{"type":"string","nullable":true,"description":"null = catálogo de la página del CRM"},"is_general":{"type":"boolean","description":"true = sirve para todos los proyectos"},"label":{"type":"string"},"message_text":{"type":"string"},"sort_order":{"type":"integer"}}}}}}}}},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/{mortgage_payment_id}/conciliate":{"put":{"summary":"Conciliar pago","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/{mortgage_payment_id}/receipt":{"get":{"summary":"Obtener recibo","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/{mortgage_payment_id}/request-invoice":{"post":{"summary":"Solicitar factura de un pago ya registrado","tags":["Mortgage Payments"],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Solicitud enviada"},"404":{"description":"Pago no encontrado"},"409":{"description":"No se puede enviar (proyecto sin receptor o sin WhatsApp)"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/{mortgage_payment_id}":{"delete":{"summary":"Eliminar appmortgage-payments","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"patch":{"summary":"Actualizar parcialmente appmortgage-payments","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Obtener appmortgage-payments por ID","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/{mortgage_payment_id}/send-notification":{"post":{"summary":"Enviar notificación","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments":{"get":{"summary":"Listar appmortgage-payments","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appmortgage-payments","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgage-payments/send-receipt":{"post":{"summary":"Enviar recibo","tags":["Pagos de Financiamiento"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgages/{mortgage_id}":{"get":{"summary":"Obtener appmortgages por ID","tags":["Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appmortgages","tags":["Financiamientos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"mortgage_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/mortgages":{"get":{"summary":"Listar appmortgages","tags":["Financiamientos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/notary/contracts":{"get":{"summary":"Listar contratos","tags":["Notaría"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/notary/contracts/status-count":{"get":{"summary":"Conteo por estado","tags":["Notaría"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/cards":{"get":{"summary":"Listar tarjetas","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear tarjetas","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar tarjetas","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/config":{"get":{"summary":"Obtener configuración de OpenPay","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/customers":{"get":{"summary":"Listar clientes","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear clientes","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/plans":{"get":{"summary":"Listar planes","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear planes","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/subscriptions":{"get":{"summary":"Listar suscripciones","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear suscripciones","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar suscripciones","tags":["OpenPay"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/webhook-logs":{"get":{"summary":"Logs de webhooks","tags":["OpenPay"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/openpay/webhooks":{"get":{"summary":"Listar webhooks","tags":["OpenPay"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear webhooks","tags":["OpenPay"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/owner-commissions/{commission_id}/payments/{payment_id}":{"delete":{"summary":"Eliminar pagos","tags":["Comisiones Propietario"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"payment_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/owner-commissions/{commission_id}/payments":{"get":{"summary":"Obtener pagos por ID","tags":["Comisiones Propietario"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear pagos","tags":["Comisiones Propietario"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/owner-commissions/{commission_id}":{"get":{"summary":"Obtener appowner-commissions por ID","tags":["Comisiones Propietario"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appowner-commissions","tags":["Comisiones Propietario"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"commission_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/password-reset/request":{"post":{"summary":"Solicitar restablecimiento de contraseña","tags":["Autenticación"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/password-reset/reset":{"post":{"summary":"Restablecer contraseña","tags":["Autenticación"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/password-reset/verify":{"post":{"summary":"Verificar token de restablecimiento","tags":["Autenticación"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/payment-methods":{"get":{"summary":"Listar apppayment-methods","tags":["Catálogos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/payment-types":{"get":{"summary":"Listar apppayment-types","tags":["Catálogos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/pipeline/brands":{"get":{"summary":"Marcas disponibles para el tablero del CRM","tags":["Pipeline CRM"],"description":"Devuelve las marcas (meta_page) que este usuario puede elegir en el kanban, cada una con vende_inventario_propio para saber qué rama de etapas le toca. Un usuario sin renglones en user_page_access recibe solo la página del CRM, que es lo que el tablero le ha mostrado siempre. Sin user_id se devuelve el catálogo completo.\n","parameters":[{"in":"query","name":"user_id","schema":{"type":"integer"},"description":"Usuario que consulta; filtra por sus accesos."}],"responses":{"200":{"description":"Marcas disponibles"},"500":{"description":"Error interno del servidor"}}}},"/api/pipeline/stages":{"get":{"summary":"Listar stages","tags":["Pipeline CRM"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page_id","schema":{"type":"string"},"description":"Marca (meta_page) para la que se piden las etapas. Determina si se devuelve la rama de inventario propio o la de intermediación. Si se omite, o si la página no existe, se responde la de inventario.\n"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/project-media/{media_id}/file":{"get":{"summary":"Servir el archivo de un medio de la galería","tags":["Chat / Conversaciones"],"description":"Responde 302 hacia una URL firmada de S3 generada al vuelo, igual que /api-virtual-agent/v1/me/avatar. El bucket es privado, así que esta URL ESTABLE es la única forma de poner la imagen en un `<img src>` sin que deje de funcionar a los cinco minutos.\nLA DIFERENCIA CON /api/file-manager/generate-download-url: ese endpoint firma cualquier key que le manden, sin credenciales — incluidas las identificaciones de los clientes. Aquí solo se firma lo que sea un renglón ACTIVO de project_media y esté bajo el prefijo project-media/. No se acepta un key desde afuera en ningún caso.\n","parameters":[{"in":"path","name":"media_id","required":true,"schema":{"type":"integer"}}],"responses":{"302":{"description":"Redirección al archivo"},"404":{"description":"El medio no existe o está dado de baja"}}}},"/api/project-media":{"get":{"summary":"Galería activa de un proyecto (para la bandeja)","tags":["Chat / Conversaciones"],"description":"Lo que ve quien está contestando una conversación: solo los medios activos, en el orden que decidió Marketing. Sigue el mismo patrón que /api/messenger-quick-replies (lectura sin guard, escritura protegida en /api/admin/project-media).\nNO devuelve file_key. La imagen se consume por /api/project-media/{media_id}/file, que es lo que impide que el navegador conozca —y pueda pedir— rutas del bucket.\n","parameters":[{"in":"query","name":"project_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Galería del proyecto"},"400":{"description":"Falta project_id"}}}},"/api/project-statuses":{"get":{"summary":"Listar appproject-statuses","tags":["Catálogos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/contracts":{"get":{"summary":"Obtener contratos por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/dashboard":{"get":{"summary":"Obtener dashboard por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/dashboard/sales-ranking":{"get":{"summary":"Ranking de ventas","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/due-annual-mortgages":{"get":{"summary":"Financiamientos anuales por vencer","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/due-mortgages":{"get":{"summary":"Financiamientos por vencer","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/financiamientos":{"get":{"summary":"Listado de financiamientos","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/inventory-status":{"get":{"summary":"Estado de inventario","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/mortgage-payments":{"get":{"summary":"Obtener mortgage-payments por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/bulk-payment":{"get":{"summary":"Obtener pago masivo","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear pago masivo","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/cancelled":{"get":{"summary":"Comisiones canceladas","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/individual-payment-detail":{"get":{"summary":"Detalle de pago individual","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/payment-history":{"get":{"summary":"Historial de pagos","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions":{"get":{"summary":"Obtener owner-commissions por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/send-statement":{"get":{"summary":"Vista previa de estado de cuenta","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Enviar estado de cuenta","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/owner-commissions/summary":{"get":{"summary":"Resumen de comisiones del propietario","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/points-by-owner":{"get":{"summary":"Polígonos del plano con el dueño de cada lote","description":"Igual que /points pero agregando quién es el dueño de cada lote, para el plano por dueño del panel de administración. El dueño sale de property → mortgage → contract → users. Los contratos CANCELADOS no cuentan como dueño: el lote regresó, así que se devuelve owner en null.\n","tags":["Projects"],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/points-v2":{"get":{"summary":"Obtener puntos (v2)","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/points":{"get":{"summary":"Obtener puntos por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear puntos","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/properties-price-list":{"get":{"summary":"Lista de precios de propiedades","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/properties":{"get":{"summary":"Obtener propiedades por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}":{"get":{"summary":"Obtener appprojects por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appprojects","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appprojects","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appprojects","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/{project_id}/users":{"get":{"summary":"Obtener usuarios por ID","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear usuarios","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/inventorycount":{"get":{"summary":"Conteo de inventario","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/points":{"get":{"summary":"Listar puntos","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear puntos","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects":{"get":{"summary":"Listar appprojects","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appprojects","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/projects/with-clicks":{"get":{"summary":"Proyectos con clicks","tags":["Proyectos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property-reservations/{reservation_id}":{"get":{"summary":"Obtener appproperty-reservations por ID","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"reservation_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appproperty-reservations","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"reservation_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appproperty-reservations","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"reservation_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property-reservations/by-property/{property_id}":{"get":{"summary":"Obtener by-property por ID","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property-reservations/refunds":{"get":{"summary":"Listar apartados sin contrato (candidatos a devolución) y los ya devueltos","tags":["Apartados"],"parameters":[{"in":"query","name":"project_id","schema":{"type":"integer"}},{"in":"query","name":"refunded","schema":{"type":"string","enum":[true,false]},"description":"true = solo devueltos, false = solo pendientes de devolución"}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/property-reservations":{"get":{"summary":"Listar appproperty-reservations","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appproperty-reservations","tags":["Reservaciones"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property-statuses":{"get":{"summary":"Listar appproperty-statuses","tags":["Catálogos"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property/{property_id}":{"get":{"summary":"Obtener appproperty por ID","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Editar financiamiento de la propiedad (recalcula precio, financiamiento y mensualidad)","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"price_per_sqm":{"type":"number"},"term_months":{"type":"integer"},"annual_payment":{"type":"number"},"down_payment":{"type":"number"}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"404":{"description":"Propiedad no encontrada"},"409":{"description":"El estatus de la propiedad no permite editar el financiamiento"},"500":{"description":"Error interno del servidor"}}}},"/api/property/{property_id}/status":{"put":{"summary":"Actualizar estado","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property/cron-update-reserved":{"get":{"summary":"Cron - actualizar reservaciones expiradas","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property":{"get":{"summary":"Listar appproperty","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property/status-available":{"get":{"summary":"Propiedades disponibles por estado","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property/update-status-v2":{"post":{"summary":"Actualizar estado de propiedad (v2)","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/property/update-status":{"post":{"summary":"Actualizar estado de propiedad","tags":["Propiedades"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/query":{"post":{"summary":"Crear appquery","tags":["Query"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Listar appquery","tags":["Query"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/quick-replies/propose":{"post":{"summary":"Proponer una respuesta rápida desde el chat (cualquier usuario)","tags":["CRM"],"security":[{"bearerAuth":[]}],"description":"Quien atiende una conversación (Mesa de ayuda, agentes) puede proponer el texto que acaba de escribir como respuesta rápida. Entra como 'pendiente' y NO aparece en el chat de nadie hasta que Marketing o Administración la apruebe desde el catálogo (PUT /api/admin/quick-replies/{quick_reply_id} con status 'aprobada'). project_id null propone una respuesta GENERAL, para todos los proyectos.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","message_text","project_id","user_id"],"properties":{"label":{"type":"string","description":"Texto del chip (1 a 100 caracteres)"},"message_text":{"type":"string","description":"Mensaje propuesto (1 a 2000 caracteres)"},"project_id":{"type":"integer","nullable":true},"user_id":{"type":"integer","description":"Quien la propone (queda en created_by)"}}}}}},"responses":{"201":{"description":"Propuesta enviada a revisión"},"400":{"description":"Solicitud inválida"},"500":{"description":"Error interno del servidor"}}}},"/api/reports/financial-dashboard":{"get":{"summary":"Dashboard financiero","tags":["Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Generar dashboard financiero","tags":["Reportes"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/reports/monthly-summary":{"get":{"summary":"Resumen mensual","tags":["Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear monthly-summary","tags":["Reportes"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/reports/promoter-properties":{"get":{"summary":"Propiedades por promotor","tags":["Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/reports/properties-by-month-promoter":{"get":{"summary":"Propiedades por mes y promotor","tags":["Reportes"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/roles":{"get":{"summary":"Listar approles","tags":["Roles"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear approles","tags":["Roles"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/seguimientos/{seguimiento_id}":{"patch":{"summary":"Corregir o cerrar un seguimiento (y encadenar el siguiente)","tags":["Leads / CRM"],"description":"Tres acciones según `accion` en el cuerpo. 'corregir' arregla un pendiente mal capturado. 'cerrar' lo cierra con un resultado del catálogo y, si viene `siguiente`, crea el próximo paso encadenado (anterior_id) EN UNA SOLA TRANSACCIÓN. 'corregir_resultado' cambia el resultado de uno ya cerrado. El resultado puede además mover la etapa del lead, siempre dentro de la misma transacción del cierre y siempre reportado con las mismas llaves (etapa_movida / etapa_nueva / motivo_no_movida / mensaje_no_movida): (a) los resultados de pérdida (desistio, fuera_de_perfil, competencia, numero_equivocado) mueven a Perdido SOLO si no hay propiedades de por medio; si las hay, la respuesta trae etapa_movida false con el motivo y el mensaje listo para mostrar. (b) contesto y pidio_info mueven a Contacto Inicial SOLO si el lead sigue en Nuevo (o sin etapa), y sellan last_contact_date igual que el endpoint de etapas. Desde cualquier otra etapa no se mueve nada y NO es un error: etapa_movida viene false y sin mensaje que mostrar. Cerrar y corregir_resultado devuelven además `resultado_tipo` (y corregir_resultado, `resultado_tipo_anterior`): en qué terminó la conversación — 'avance' (el cliente aceptó algo concreto: hoy solo quiere_visita), 'continuacion' (se habló y no se comprometió a nada: contesto, no_contesto, pidio_info, reprogramar y confirma_el) o 'cierre' (terminó: gano y los cuatro de pérdida). El tipo se DEDUCE del resultado, no se guarda, y viene también dentro del objeto `seguimiento`. Es lo que cuenta `racha_sin_avance` en GET /api/seguimientos.\n","parameters":[{"in":"path","name":"seguimiento_id","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["accion","user_id"],"properties":{"accion":{"type":"string","enum":["corregir","cerrar","corregir_resultado"]},"user_id":{"type":"integer"},"agendado_para":{"type":"string"},"actividad":{"type":"string"},"nota":{"type":"string"},"resultado":{"type":"string"},"resultado_nota":{"type":"string"},"siguiente":{"type":"object","properties":{"agendado_para":{"type":"string"},"actividad":{"type":"string"},"nota":{"type":"string"}}}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"403":{"description":"El usuario no tiene acceso a la marca del lead"},"404":{"description":"Seguimiento no encontrado"},"409":{"description":"El seguimiento no está en el estado que la acción requiere"},"500":{"description":"Error interno del servidor"},"503":{"description":"El módulo no está migrado en este entorno"}}}},"/api/seguimientos":{"get":{"summary":"Lista de trabajo — pendientes y conversaciones a la deriva","tags":["Leads / CRM"],"description":"Devuelve { pendientes, deriva }. `pendientes` son los seguimientos abiertos del ámbito pedido, ordenados por agendado_para ASC y con todo lo que la app necesita para pintar la lista sin pedir nada más (nombre del cliente, agente, canal y marca). Cada pendiente trae además `racha_sin_avance` (entero, nunca null): cuántos seguimientos ya cerrados de ESE lead terminaron en 'continuacion' —se habló y el cliente no se comprometió a nada: contesto, no_contesto, pidio_info, reprogramar, confirma_el— desde el último que sí avanzó (quiere_visita), o desde el principio si nunca hubo uno. Un cierre (gano o los cuatro de pérdida) también corta la racha, y los cerrados sin resultado ni cuentan ni cortan. El tipo se deduce del resultado guardado, no hay columna nueva. 0 significa \"ya avanzó o es la primera vez\"; 3 o más es un lead que se está cayendo mientras parece atendido. Con scope=equipo agrega `agentes` (agent_id, agente, pendientes, atrasados, deriva) para armar el filtro por agente sin un segundo viaje; `deriva` de ese catálogo cuenta solo los renglones devueltos (topados), es un piso y no un total. `deriva` solo viene con incluir_deriva=1 y son las conversaciones activas SIN pendiente que ya no esperan respuesta nuestra — las que nadie va a abrir nunca porque no salen en ninguna otra lista.\n","parameters":[{"in":"query","name":"user_id","required":true,"schema":{"type":"integer"}},{"in":"query","name":"scope","required":false,"schema":{"type":"string","enum":["mios","equipo"]},"description":"'mios' (por omisión) filtra por leads.agent_id. 'equipo' devuelve todos, pero SOLO para Marketing y Administración (403 en cualquier otro caso) y solo de las marcas a las que el usuario tiene acceso (default CERRADO: sin accesos, listas vacías).\n"},{"in":"query","name":"agent_id","required":false,"schema":{"type":"integer"},"description":"Filtro por agente del módulo de Marketing. Solo aplica con scope=equipo; con scope=mios se ignora.\n"},{"in":"query","name":"incluir_deriva","required":false,"schema":{"type":"string"},"description":"1 para incluir las conversaciones a la deriva"}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"403":{"description":"Pidió scope=equipo sin el rol para verlo (sin_permiso_equipo=true)\n"},"500":{"description":"Error interno del servidor"},"503":{"description":"El módulo no está migrado en este entorno"}}}},"/api/signature-notification-config":{"get":{"summary":"Configuración de destinatarios de notificaciones de propuestas de fecha de firma","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Config actual (user_id por tipo) y usuarios elegibles (rol Administración activos)"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Guardar destinatario de notificaciones por tipo de firma","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"notaria_user_id":{"type":"integer","nullable":true},"digital_user_id":{"type":"integer","nullable":true}}}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Usuario inválido (sin rol Cierres, inactivo o sin teléfono)"},"500":{"description":"Error interno del servidor"}}}},"/api/signature-proposals/{proposal_id}":{"put":{"summary":"Actualizar fecha, cancelar (agente) o autorizar/rechazar (administrador) una propuesta de fecha de firma pendiente","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"proposal_id","required":true,"schema":{"type":"integer"},"description":"ID de la propuesta"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["update_date","cancel","approve","reject"],"description":"update_date/cancel las ejecuta el agente dueño de la propuesta; approve/reject requieren rol Administrador"},"user_id":{"type":"integer"},"proposed_date_time":{"type":"string","example":"2026-07-23 11:00:00","description":"Solo para update_date"},"review_notes":{"type":"string","description":"Motivo del rechazo (requerido para reject)"},"skip_notification":{"type":"boolean","description":"Solo para approve. Si es true, no se envía WhatsApp al agente"}}}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida (action inválida, fecha inválida, slot lleno o review_notes faltante en reject)"},"401":{"description":"No autorizado"},"403":{"description":"La propuesta no pertenece al agente (update_date/cancel) o el usuario no es administrador (approve/reject)"},"404":{"description":"Propuesta no encontrada"},"409":{"description":"La propuesta ya no está pendiente"},"500":{"description":"Error interno del servidor"}}}},"/api/signature-proposals":{"post":{"summary":"Crear propuesta de fecha de firma para un contrato en estatus Nuevo","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contract_id":{"type":"integer"},"proposed_date_time":{"type":"string","example":"2026-07-23 11:00:00"},"user_id":{"type":"integer"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Propuesta creada","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida (campos faltantes, fecha pasada o slot inválido/lleno)"},"401":{"description":"No autorizado"},"403":{"description":"El contrato no pertenece al agente"},"404":{"description":"Contrato no encontrado"},"409":{"description":"Ya existe una propuesta pendiente para el contrato"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Listar propuestas de fecha de firma por estatus (para el módulo de autorización de administración)","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["PENDING","APPROVED","REJECTED","CANCELLED"],"default":"PENDING"},"description":"Estatus de las propuestas a listar (default PENDING)"}],"responses":{"200":{"description":"Listado de propuestas ordenado de la más antigua a la más reciente","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"array","items":{"type":"object","properties":{"proposal_id":{"type":"integer"},"contract_id":{"type":"integer"},"proposed_date_time":{"type":"string","example":"2026-07-23 11:00:00"},"notes":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"proposed_by":{"type":"integer"},"agent_name":{"type":"string"},"customer_name":{"type":"string"},"project_id":{"type":"integer"},"project_name":{"type":"string"},"properties":{"type":"string"},"signature_type":{"type":"string"},"contract_status_code":{"type":"string"}}}}}}}}},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/signature-schedule/available-slots":{"get":{"summary":"Obtener horarios disponibles","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/signature-schedule":{"get":{"summary":"Listar appsignature-schedule","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appsignature-schedule","tags":["Agenda de Firmas"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/stripe/charge-logs":{"get":{"summary":"Historial de cargos automaticos de Stripe por financiamiento","description":"Bitacora de los eventos de Stripe (webhooks) asociados a un financiamiento. Solo para roles Administrador (3) y Cobranza (11). Filtra por el entorno actual (test / live) para que las pruebas nunca se mezclen con produccion.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Falta mortgage_id"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos"},"500":{"description":"Error interno del servidor"}}}},"/api/stripe/config":{"get":{"summary":"Configuración pública de Stripe (clave publicable y modo)","description":"Devuelve únicamente datos públicos para inicializar Stripe.js en el navegador. La secret key NUNCA sale del servidor.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"publishable_key":{"type":"string"},"livemode":{"type":"boolean"},"api_version":{"type":"string"},"stripe_js_url":{"type":"string"}}}}}}}},"401":{"description":"No autenticado"},"500":{"description":"Stripe no está configurado"}}}},"/api/stripe/payment-methods":{"get":{"summary":"Listar las tarjetas guardadas del titular de un financiamiento","tags":["Stripe"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos sobre el financiamiento"},"404":{"description":"Financiamiento no encontrado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Confirmar un SetupIntent y guardar la tarjeta enmascarada","description":"Recibe el id del SetupIntent que el navegador ya confirmó con Stripe.js. Sólo se persisten datos enmascarados (marca, últimos 4, vigencia, tipo y país). El número de tarjeta nunca llega al servidor.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mortgage_id","setup_intent_id","mandato_aceptado"],"properties":{"mortgage_id":{"type":"integer"},"setup_intent_id":{"type":"string"},"mandato_aceptado":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Tarjeta guardada"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos sobre el financiamiento"},"404":{"description":"Financiamiento no encontrado"},"500":{"description":"Error interno del servidor"},"502":{"description":"Error de Stripe"}}},"delete":{"summary":"Dar de baja una tarjeta (detach en Stripe)","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mortgage_id","stripe_payment_method_id"],"properties":{"mortgage_id":{"type":"integer"},"stripe_payment_method_id":{"type":"integer"},"cancelar_suscripciones":{"type":"boolean","description":"Opcional. Si es true, cancela también las domiciliaciones vigentes que usaban la tarjeta. Si se omite y hay alguna, responde 409 TARJETA_EN_USO.\n"}}}}}},"responses":{"200":{"description":"Tarjeta eliminada"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos sobre el financiamiento"},"404":{"description":"Tarjeta no encontrada"},"409":{"description":"La tarjeta está en uso por una domiciliación vigente"},"500":{"description":"Error interno del servidor"}}}},"/api/stripe/retry-payment":{"get":{"summary":"Estado de la factura pendiente de una domiciliacion","description":"Devuelve la factura que quedo impaga (si la hay) con su motivo de rechazo y su enlace de pago, para que administracion decida si reintenta el cobro o le reenvia el enlace al cliente.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos"},"404":{"description":"Sin domiciliación vigente"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Reintentar el cobro de la factura pendiente, o reenviar su enlace","description":"accion='cobrar' paga la MISMA factura que Stripe viene reintentando (invoices.pay). Nunca crea un cargo nuevo: hacerlo duplicaria el cobro cuando el reintento automatico de Stripe entre despues. accion='enlace' le reenvia al cliente el enlace de pago de esa factura, para que pague cuando tenga fondos o con otra tarjeta. El pago NO se registra aqui: lo registra el webhook al recibir invoice.paid, que es el mismo camino de un cobro automatico exitoso.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mortgage_id":{"type":"integer"},"accion":{"type":"string","enum":["cobrar","enlace"]}}}}}},"responses":{"200":{"description":"Cobro aplicado o enlace enviado"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"402":{"description":"La tarjeta fue rechazada o requiere autenticación"},"403":{"description":"Sin permisos"},"404":{"description":"Sin domiciliación o sin factura pendiente"},"409":{"description":"La factura no está en un estado cobrable"},"500":{"description":"Error interno del servidor"},"502":{"description":"Error de Stripe"}}}},"/api/stripe/setup-intent":{"post":{"summary":"Crear un SetupIntent para guardar una tarjeta (domiciliación Stripe)","description":"Primer paso del alta de tarjeta. Devuelve el client_secret que el navegador usa con el Payment Element. El número de tarjeta nunca llega al servidor.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mortgage_id"],"properties":{"mortgage_id":{"type":"integer"}}}}}},"responses":{"201":{"description":"SetupIntent creado","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos sobre el financiamiento"},"404":{"description":"Financiamiento no encontrado"},"409":{"description":"Financiamiento inactivo"},"500":{"description":"Error interno del servidor"},"502":{"description":"Error de Stripe"}}}},"/api/stripe/subscriptions":{"get":{"summary":"Estado de la domiciliacion (suscripcion vigente) de un financiamiento","description":"Devuelve la suscripcion vigente del financiamiento (o null). Solo el titular del contrato o los roles Administrador (3) / Cobranza (11).\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"mortgage_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","nullable":true}}}}}},"400":{"description":"Falta mortgage_id"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos"},"404":{"description":"Financiamiento no encontrado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Activar la domiciliacion (crea Product + Price + Subscription en Stripe)","description":"Ancla el primer cobro al dia de pago del financiamiento y programa el cancel_at para que la suscripcion termine junto con el plazo. Las mensualidades ya vencidas NO se cobran de forma retroactiva: se reportan en `aviso` para que cobranza las registre a mano.\n","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mortgage_id":{"type":"integer"},"stripe_payment_method_id":{"type":"integer"},"mandato_aceptado":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Domiciliación activada"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos"},"404":{"description":"Financiamiento no encontrado"},"409":{"description":"Regla de negocio incumplida"},"500":{"description":"Error interno del servidor"},"502":{"description":"Error de Stripe"}}},"delete":{"summary":"Desactivar la domiciliacion","tags":["Stripe"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mortgage_id":{"type":"integer"},"stripe_subscription_id":{"type":"integer"},"motivo":{"type":"string"}}}}}},"responses":{"200":{"description":"Domiciliación desactivada"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autenticado"},"403":{"description":"Sin permisos"},"404":{"description":"Suscripción no encontrada"},"500":{"description":"Error interno del servidor"},"502":{"description":"Error de Stripe"}}}},"/api/stripe/webhooks":{"get":{"summary":"Ping de salud del webhook de Stripe","tags":["Stripe"],"responses":{"200":{"description":"Endpoint activo"}}},"post":{"summary":"Recibe los eventos de Stripe (firmados con HMAC)","tags":["Stripe"],"responses":{"200":{"description":"Evento recibido (procesado, duplicado o ignorado)"},"400":{"description":"Firma invalida o webhook no configurado"},"500":{"description":"No se pudo registrar el evento (Stripe debe reintentar)"}}}},"/api/swagger":{"get":{"summary":"Obtener especificación OpenAPI","tags":["Documentación"],"responses":{"200":{"description":"Especificación OpenAPI completa","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/track-click":{"post":{"summary":"Crear apptrack-click","tags":["Tracking"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/track-conversion":{"post":{"summary":"Crear apptrack-conversion","tags":["Tracking"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users-v2/{user_id}":{"get":{"summary":"Obtener appusers-v2 por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/assign-project":{"post":{"summary":"Asignar proyecto","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/assign-role":{"post":{"summary":"Asignar rol","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/contracts":{"get":{"summary":"Obtener contratos por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/crm-summary":{"get":{"summary":"Resumen CRM del usuario","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/identification":{"put":{"summary":"Actualizar identificación","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar identificación","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/projects/{project_id}":{"delete":{"summary":"Eliminar proyectos","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"project_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/projects":{"get":{"summary":"Obtener proyectos por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear proyectos","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar proyectos","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/promoter-contracts/{contract_id}":{"put":{"summary":"Actualizar promoter-contracts","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar promoter-contracts","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"contract_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/promoter-contracts":{"get":{"summary":"Obtener promoter-contracts por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear promoter-contracts","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/roles/{userRoleId}":{"delete":{"summary":"Eliminar roles","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"},{"in":"path","name":"userRoleId","required":true,"schema":{"type":"string"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/roles":{"get":{"summary":"Obtener roles por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}":{"get":{"summary":"Obtener appusers por ID","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"put":{"summary":"Actualizar appusers","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"delete":{"summary":"Eliminar appusers","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/toggle-status":{"put":{"summary":"Activar/desactivar","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/{user_id}/transfer-clients":{"post":{"summary":"Transferir clientes","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Obtener transferencia de clientes","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users/active-promoter":{"get":{"summary":"Promotor activo","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/users":{"get":{"summary":"Listar appusers","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear appusers","tags":["Usuarios"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/virtual-agents/{virtual_agent_id}/regenerate-key":{"post":{"summary":"Regenerar la API key del agente virtual","description":"La key anterior deja de funcionar de inmediato: hay que actualizarla en el servicio externo que consume /api-virtual-agent/v1/*.\n","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"virtual_agent_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Agente no encontrado"},"500":{"description":"Error interno del servidor"}}}},"/api/virtual-agents/{virtual_agent_id}":{"get":{"summary":"Detalle de un agente virtual","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"virtual_agent_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Agente no encontrado"}}},"put":{"summary":"Actualizar datos del agente virtual y sus proyectos","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"virtual_agent_id","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Solicitud inválida"},"404":{"description":"Agente no encontrado"},"409":{"description":"Email ya usado por otro usuario"}}},"delete":{"summary":"Eliminar el agente virtual y desactivar su usuario","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"virtual_agent_id","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Operación exitosa"},"404":{"description":"Agente no encontrado"}}}},"/api/virtual-agents":{"get":{"summary":"Listar agentes virtuales con sus proyectos asignados","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa"},"401":{"description":"No autenticado"},"403":{"description":"Solo administradores"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear agente virtual (crea también su usuario y su API key)","tags":["Agente Virtual"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["first_name","email"],"properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"email":{"type":"string"},"phone_number":{"type":"string"},"description":{"type":"string"},"project_ids":{"type":"array","items":{"type":"integer"}}}}}}},"responses":{"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"409":{"description":"Ya existe un usuario con ese email"},"500":{"description":"Error interno del servidor"}}}},"/api/visit-slots":{"get":{"summary":"Próximos horarios libres de un promotor para dar recorrido","tags":["Leads / CRM"],"description":"Expande la disponibilidad del promotor (agent_availability) sobre los próximos días, le descuenta lo que ya tiene agendado y descarta lo que caiga demasiado pronto. En cada fecha manda lo que se le haya capturado PARA ESE DÍA (specific_date) y, solo si no tiene captura propia, su plantilla semanal. Cada hueco viene con las tres etiquetas ya redactadas para no tener que armar la frase en el frontend: 'etiqueta' es la que se pega en el mensaje al cliente ('el sábado 16 a las 10'), 'dia_etiqueta' es el encabezado del grupo ('Sáb 16') y 'hora_etiqueta' el texto de la pastilla, en 24 h ('10:00').\nUna lista vacía NO es un error: significa que ese promotor no tiene horarios capturados o que los próximos ya están ocupados.\n","parameters":[{"in":"query","name":"user_id","required":true,"schema":{"type":"integer"},"description":"Promotor que daría el recorrido."},{"in":"query","name":"dias","required":false,"schema":{"type":"integer"},"description":"Horizonte en días (por omisión 21, máximo 60)."},{"in":"query","name":"limite","required":false,"schema":{"type":"integer"},"description":"Cuántos huecos devolver (por omisión 12, máximo 50)."}],"responses":{"200":{"description":"Operación exitosa"},"400":{"description":"Falta user_id o no es válido"},"500":{"description":"Error interno del servidor"}}}},"/api/visits/{appointment_id}":{"patch":{"summary":"Cambiar el estado de una cita de visita","tags":["Leads / CRM"],"description":"Mueve la cita a 'confirmada', 'cancelada', 'cumplida' o 'no_asistio'.\nAl CONFIRMAR, además de la cita, mueve el lead a la etapa Visita Programada (status_id = 4): es lo que hace que esa columna del kanban por fin contenga una fecha real y no un \"ya quedamos\". Solo se mueve si el lead viene de Nuevo Lead, Contacto Inicial o Postpuesto; a un lead que ya va en Reservación, Apartado o Ganado no se le regresa la etapa.\nConfirmar también puede chocar contra el candado de agenda (el mismo índice único parcial que protege el alta): si otro cliente ya ocupó ese hueco con ese promotor, responde 409.\n","parameters":[{"in":"path","name":"appointment_id","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["confirmada","cancelada","cumplida","no_asistio"]},"user_id":{"type":"integer","description":"Quién hace el cambio (para la bitácora del lead)"}}}}}},"responses":{"200":{"description":"Cita actualizada"},"400":{"description":"Solicitud inválida"},"404":{"description":"Cita no encontrada"},"409":{"description":"Ese horario ya está ocupado para ese promotor"},"500":{"description":"Error interno del servidor"},"503":{"description":"Falta aplicar la migración de visitas"}}}},"/api/webhooks/docspring/submission":{"post":{"summary":"Webhook de DocSpring","tags":["Webhooks"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/webhooks/meta-leads":{"get":{"summary":"Verificación de suscripción del webhook de Meta Lead Ads","tags":["Leads / CRM"],"description":"Meta llama a este endpoint con hub.mode=subscribe, hub.verify_token y hub.challenge al registrar el webhook. Devolvemos el challenge si el token coincide.\n","responses":{"200":{"description":"Devuelve hub.challenge en texto plano"},"403":{"description":"verify_token inválido"}}},"post":{"summary":"Recepción de leads de Meta Lead Ads (Instant Forms)","tags":["Leads / CRM"],"description":"Recibe la notificación leadgen de Meta, descarga los datos del lead vía Graph API, crea/encuentra el cliente (tabla users) y crea el lead (tabla leads) con lead_source_id = Facebook Meta Ads. Idempotente por meta_leadgen_id (UNIQUE). El mismo callback recibe los eventos de Messenger (object 'page') y los DMs de Instagram (object 'instagram', campo messages): ambos crean/actualizan leads de chat (meta_psid / instagram_igsid) y registran la conversación. También recibe los COMENTARIOS de la página (campo 'feed'), que pasan por dos flujos con interruptor propio: moderación con IA (COMMENT_MODERATION_MODE = observacion|activo, oculta los NEGATIVO) y respuesta privada a los de INTERES (PRIVATE_REPLY_MODE = off|observacion|activo). En 'activo' la respuesta privada manda UN mensaje por Messenger al autor (irreversible y único por comentario) y con el PSID que devuelve Graph crea el cliente, el lead y la conversación, y avisa al agente asignado. A cada PERSONA se le escribe una sola vez —nunca a quien ya tiene conversación abierta— y el volumen está topado por hora (PRIVATE_REPLY_MAX_POR_HORA, default 10). Además, y con interruptor propio (PUBLIC_REPLY_MODE = off|observacion|activo), los de INTERES reciben una respuesta PÚBLICA (comentario hijo visible, sin cifras) que sirve de prueba social — una sola por comentario, para siempre.\n","responses":{"200":{"description":"Evento procesado"},"401":{"description":"Firma X-Hub-Signature-256 inválida o secreto no configurado"},"500":{"description":"Error interno del servidor"}}}},"/api/webhooks/whatsapp-cloud":{"get":{"summary":"Verificación de suscripción del webhook de WhatsApp Cloud API","tags":["Leads / CRM"],"description":"Meta llama con hub.mode=subscribe, hub.verify_token y hub.challenge al registrar el webhook. Devolvemos el challenge si el token coincide.\n","responses":{"200":{"description":"Devuelve hub.challenge en texto plano"},"403":{"description":"verify_token inválido"}}},"post":{"summary":"Mensajes y acuses de WhatsApp Cloud API (chat del CRM)","tags":["Leads / CRM"],"description":"Recibe mensajes entrantes de las LÍNEAS del catálogo whatsapp_line (multilínea 2026-08-19) y los acuses de los mensajes salientes. Un mensaje de un lead existente (por conversación previa o por teléfono del cliente) se registra en su chat; un número desconocido crea cliente + lead con la marca (page_id) y la línea (whatsapp_phone_id) por la que escribió (fuente WhatsApp; si viene de un anuncio click-to-WhatsApp, con la lista de Mesa de ayuda del proyecto de ese anuncio). Línea con recibe_en_crm=false → 'linea-sin-bandeja'; línea desconocida o apagada → 'otro-numero'. Los acuses 'failed' marcan el mensaje como NO entregado y avisan al agente, porque la Cloud API acepta el envío y falla después de forma asíncrona. Idempotente por wamid. Firma X-Hub-Signature-256 con META_APP_SECRET.\n","responses":{"200":{"description":"Evento procesado"},"401":{"description":"Firma inválida o secreto no configurado"},"500":{"description":"Error interno del servidor"}}}},"/api/whatsapp/messages/{user_id}":{"get":{"summary":"Obtener messages por ID","tags":["WhatsApp"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"user_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/whatsapp/send-by-project":{"get":{"summary":"Obtener configuración WhatsApp por proyecto","tags":["WhatsApp"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Enviar mensaje WhatsApp por proyecto","tags":["WhatsApp"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/whatsapp/send":{"get":{"summary":"Listar send","tags":["WhatsApp"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Crear send","tags":["WhatsApp"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api/whatsapp/templates":{"get":{"summary":"Catálogo de plantillas de WhatsApp","tags":["WhatsApp"],"description":"Devuelve las plantillas del catálogo local. Con sync=1 consulta primero el estado real en Meta y actualiza status, categoría y motivo de rechazo — útil después de dar de alta una, porque la aprobación puede tardar horas. Con approved_only=1 solo las que ya se pueden enviar.\n","parameters":[{"in":"query","name":"sync","schema":{"type":"string"},"description":"Con valor 1 refresca los estados desde Meta"},{"in":"query","name":"purpose","schema":{"type":"string"},"description":"Filtra por propósito (reactivacion_general, seguimiento_visita…)"},{"in":"query","name":"approved_only","schema":{"type":"string"},"description":"Con valor 1 solo devuelve las APPROVED"}],"responses":{"200":{"description":"Operación exitosa"},"500":{"description":"Error interno del servidor"}}},"post":{"summary":"Dar de alta una plantilla en Meta y guardarla en el catálogo","tags":["WhatsApp"],"description":"Crea la plantilla en la cuenta de WhatsApp Business y la guarda como PENDING. Meta la revisa aparte y PUEDE RECATEGORIZARLA (mandar UTILITY y que quede MARKETING, que cuesta 3.6 veces más): se guarda la categoría que Meta devolvió, no la que se pidió. No envía ningún mensaje.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"language_code":{"type":"string"},"category":{"type":"string","enum":["UTILITY","MARKETING"]},"purpose":{"type":"string"},"label":{"type":"string"},"body_text":{"type":"string"},"variables":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"description":{"type":"string"},"example":{"type":"string"}}}},"user_id":{"type":"integer"}}}}}},"responses":{"201":{"description":"Plantilla creada y en revisión"},"400":{"description":"Solicitud inválida"},"409":{"description":"Ya existe una plantilla con ese nombre e idioma"},"500":{"description":"Error interno del servidor"},"502":{"description":"Meta rechazó el alta"}}}},"/api/whatsapp/webhook":{"post":{"summary":"Webhook de WhatsApp","tags":["WhatsApp"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"201":{"description":"Recurso creado exitosamente"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}},"get":{"summary":"Verificar webhook de WhatsApp","tags":["WhatsApp"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/contract":{"post":{"summary":"Crear un contrato de compra-venta y generar su PDF en DocSpring","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Envuelve el mismo flujo de dos pasos que usa la webapp de promotores (POST + PUT /api/contracts): crea el contrato y sus mortgages, y dispara la generación del PDF vía DocSpring según la plantilla del proyecto. El contrato queda atribuido al usuario del agente virtual y en estado \"Para Firma Digital\". signing_owner_id se resuelve automáticamente al owner_id del proyecto de la propiedad.\n","tags":["Agente Virtual"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id","property_ids","with_annual_payment"],"properties":{"customer_id":{"type":"integer"},"beneficiary_id":{"type":"integer","nullable":true,"description":"Si se omite, se usa el propio customer_id como beneficiario - las plantillas de DocSpring exigen datos de beneficiario y no aceptan el campo vacío.\n"},"property_ids":{"type":"array","items":{"type":"integer"},"description":"IDs de las propiedades/lotes incluidos en este contrato"},"with_annual_payment":{"type":"boolean","description":"Elegido por el cliente en la conversación: si el plan de pagos incluye un pago anual adicional o no.\n"},"signature_datetime":{"type":"string","format":"date-time","description":"Si se omite, se usa el momento actual."},"contract_sheet_count":{"type":"integer"},"client_signature_base64":{"type":"string","nullable":true,"description":"Imagen de la firma manuscrita del cliente (base64, sin el prefijo data:image/...;base64,), capturada en el chat. La firma del apoderado es siempre tipográfica y se resuelve automáticamente con el owner_id del proyecto - no requiere ningún dato adicional en el body.\n"}}}}}},"responses":{"201":{"description":"Contrato creado y PDF generado"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"403":{"description":"Proyecto no asignado al agente"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/customer":{"post":{"summary":"Crear o actualizar un cliente (buscar-o-crear por email)","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Busca un usuario existente por email; si existe, actualiza sus datos (sin borrar campos ya guardados que esta llamada no incluya). Si no existe, lo crea con rol Cliente. La dirección se mapea a las columnas de texto libre ya existentes (domicilio/ciudad_residencia/codigo_postal) - no existe columna separada para \"estado\", así que si se manda `state` se concatena dentro de domicilio. Las fotos de identificación (ine_front_url/ine_back_url, ya subidas al bucket S3 propio del agente conversacional) se guardan en file_attachments, no en user_identification (esa tabla solo soporta una imagen por fila).\n","tags":["Agente Virtual"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","first_name","last_name"],"properties":{"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"postal_code":{"type":"string"},"identification_type":{"type":"string","description":"ej. INE"},"identification_number":{"type":"string"},"ine_front_url":{"type":"string"},"ine_back_url":{"type":"string"}}}}}},"responses":{"200":{"description":"Cliente actualizado"},"201":{"description":"Cliente creado"},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/inventory":{"get":{"summary":"Listar inventario disponible de los proyectos asignados al agente virtual","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`) con la API key del agente virtual. Solo devuelve propiedades disponibles de los proyectos asignados al agente en Configuración → Agente Virtual.\n","tags":["Agente Virtual"],"parameters":[{"in":"query","name":"project_id","schema":{"type":"integer"},"description":"Acota a un proyecto concreto (debe estar asignado al agente)"},{"in":"query","name":"limit","schema":{"type":"integer"},"description":"Máximo de resultados (default 200, tope 500)"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"403":{"description":"Proyecto no asignado al agente"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/me/avatar":{"get":{"summary":"Foto de perfil del agente virtual","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Responde 302 hacia una URL firmada de S3 generada al vuelo. El bucket es privado, así que esta URL estable es la forma de consumir la imagen: el servicio externo la guarda una vez y nunca caduca.\n","tags":["Agente Virtual"],"responses":{"302":{"description":"Redirección a la imagen"},"401":{"description":"No autorizado"},"404":{"description":"El agente no tiene foto de perfil"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/me":{"get":{"summary":"Identidad y alcance del agente virtual","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Es la llamada de arranque del servicio externo: devuelve quién es el agente (nombre, email, teléfono, descripción) y qué proyectos puede ofrecer. No hay login ni token: la API key ya es la credencial, y con ella este endpoint resuelve identidad y alcance de una sola vez.\n","tags":["Agente Virtual"],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"agent":{"type":"object"},"projects":{"type":"array","items":{"type":"object"}}}}}}},"401":{"description":"No autorizado"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/property/{property_id}":{"get":{"summary":"Obtener propiedad por ID","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Solo devuelve propiedades de los proyectos asignados al agente virtual.\n","tags":["Agente Virtual"],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"403":{"description":"Proyecto no asignado al agente"},"404":{"description":"Propiedad no encontrada"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/release/{property_id}":{"post":{"summary":"Liberar una propiedad reservada por el agente virtual","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Cancela la reservación activa creada por ESTE agente virtual y regresa la propiedad a Disponible. Mismo comportamiento que /api/property/update-status-v2 (código 1), pero acotado al agente que hizo la reservación original: no se puede liberar una reservación de otro agente o de un promotor humano.\n","tags":["Agente Virtual"],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Propiedad liberada"},"400":{"description":"La propiedad no está reservada"},"401":{"description":"No autorizado"},"403":{"description":"Proyecto no asignado al agente, o la reservación pertenece a otro agente"},"404":{"description":"Propiedad no encontrada, o sin reservación activa de este agente"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/reserve/{property_id}":{"post":{"summary":"Reservar una propiedad a nombre del agente virtual","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Crea una reservación real (property_reservation con status RESERVED) y deja la propiedad en estado Reservado, atribuida al usuario del agente virtual. Solo funciona sobre propiedades disponibles de los proyectos asignados al agente.\n","tags":["Agente Virtual"],"parameters":[{"in":"path","name":"property_id","required":true,"schema":{"type":"integer"},"description":"ID del recurso"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_name","customer_email","customer_phone"],"properties":{"customer_name":{"type":"string"},"customer_email":{"type":"string"},"customer_phone":{"type":"string"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Reservación creada"},"400":{"description":"Solicitud inválida o propiedad no disponible"},"401":{"description":"No autorizado"},"403":{"description":"Proyecto no asignado al agente"},"404":{"description":"Propiedad no encontrada"},"409":{"description":"La propiedad ya tiene una reservación activa"},"500":{"description":"Error interno del servidor"}}}},"/api-virtual-agent/v1/user":{"get":{"summary":"Buscar usuario por email","description":"Requiere el header `x-api-key` (o `Authorization: Bearer <api_key>`). Sirve para que el agente virtual sepa si el prospecto ya existe en el sistema antes de pedirle otra vez sus datos.\n","tags":["Agente Virtual"],"parameters":[{"in":"query","name":"email","required":true,"schema":{"type":"string"},"description":"Email del usuario a buscar"}],"responses":{"200":{"description":"Operación exitosa","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Solicitud inválida"},"401":{"description":"No autorizado"},"404":{"description":"Usuario no encontrado"},"500":{"description":"Error interno del servidor"}}}}}}