| name | alta-cliente |
| description | Da de alta un nuevo afiliado en redesagi_facturacion. Pide datos al usuario, resuelve sucursal/area/ciudad/departamento y plan del catalogo, distingue nodos estaticos (IP) de PPPoE (usuario/clave), muestra resumen para aprobacion explicita y ejecuta public/tools/add_afiliado.php. Usar cuando el usuario diga "dar de alta", "nuevo cliente", "agregar afiliado", "registrar usuario" o similar. |
Skill: alta-cliente
Flujo guiado para crear un nuevo afiliado + factura inicial via public/tools/add_afiliado.php. La spec fuente vive en public/docs/alta_afiliados.md; el script es la autoridad (si la guia y el script discrepan, gana el script).
Producto multi-tenant / multi-sucursal: una instancia sirve a varios ISP (id-empresa) y cada empresa puede tener varias sucursales físicamente aisladas (id-grupos-empresa, arquitectura B). Cada sucursal es estática (IP fija) o PPPoE (usuario/clave), según grupos_empresa.tipo. No asumir empresa 1 / Meta / estático: resolver todo desde la BD por tenant. Por defecto --empresa-id=1 (Omar) si el usuario no indica empresa.
Paso 0 — Resolver tenant, sucursal y tipo de nodo (SIEMPRE primero)
Antes de pedir datos hay que saber a qué sucursal entra el cliente y si es estática o PPPoE, porque eso cambia qué datos pedir (IP vs usuario PPP).
- Empresa:
--empresa-id (default 1). Si el usuario opera otro ISP, pedirlo.
- Sucursales de la empresa:
SELECT `id-grupo-empresa`, descripcion
FROM grupos_empresa_vpn_target
WHERE `id-empresa` = <empresa-id>;
⚠️ La columna en esta tabla es id-grupo-empresa (singular), distinta de afiliados.id-grupos-empresa (plural).
- 1 fila → mono-sucursal: usar esa, NO pasar el flag, NO preguntar (flujo de Omar intacto).
- >1 fila → multi-sucursal: preguntar al usuario a qué sucursal (mostrar
id-grupo-empresa + descripcion). Ese grupo se pasa como --id-grupos-empresa=N. El script aborta si falta o si el grupo no pertenece a la empresa.
- Tipo de la sucursal elegida:
SELECT tipo FROM grupos_empresa WHERE id = <id-grupos-empresa>;
Esto define la rama del Paso 1.
Identidades de cobro/facturación (ajustes de sesión)
Dos identidades del tenant que el cliente nuevo debe traer para que aparezcan bien en el modal de editar. Suelen ser fijas por sesión (el operador las define al empezar un batch y se repiten para todos los clientes de ese día):
- "Facturar a nombre de" →
id-formato-factura (QUIÉN factura: razón social/NIT del PDF; tabla formato_factura). Default: FormatoFactura::defaultForEmpresa (empresa 1 → 5 = Omar Hernandez Diaz). Override de sesión: --id-formato-factura=N.
- "A dónde paga (Nequi)" →
id-recaudador (Nequi/cuenta del mensaje de WhatsApp; tabla recaudadores). Default: Recaudador::defaultForEmpresa (el super del tenant; empresa 1 → 1 = Omar Hernandez 3147654655). Override de sesión: --id-recaudador=N.
Si el operador indica una identidad para la sesión, resuelve su id una vez (consultando formato_factura / recaudadores del tenant) y pásalo en --id-formato-factura / --id-recaudador para todos los clientes de ese batch. El script valida que el id pertenezca al tenant (guard IDOR) y aborta si no.
Listar para resolver el id:
SELECT id, representante, nit FROM formato_factura;
SELECT id, nombre, nequi FROM recaudadores WHERE `id-empresa`=<emp> AND activo=1;
Las perillas Factura por WhatsApp (whatsapp) y Llamadas de cobranza (outbound_call) ya nacen habilitadas (default de columna = 1); no hay que hacer nada para que aparezcan activas.
Conexion MySQL desde CLI (patron canonico — ver CLAUDE.md):
php -r 'require_once "/var/www/ispexperts/login/db.php"; $c=new mysqli($server,$db_user,$db_pwd,$db_name); mysqli_set_charset($c,"utf8"); $r=$c->query("SELECT ..."); while($row=$r->fetch_assoc()) print_r($row);'
Paso 1 — Pedir datos al usuario
Pregunta de una sola vez (no uno por uno). Obligatorios siempre:
- nombre, apellido, cedula
- direccion, telefono
- corte (1–28; febrero limita el máximo a 28 — no 1-31)
- area_id (de la sucursal — ver Paso 2)
- id-plan: un plan activo del catálogo de la sucursal (ver Paso 2). El catálogo es obligatorio desde 2026-05-20; el script aborta si falta
--id-plan. El plan define velocidad/valor/tipo — el script los toma del catálogo (no se pasan a mano).
- estado-factura:
abierta (cliente aun no paga) o cerrada (ya pago al instalarse). Preguntar siempre.
Según el tipo de la sucursal (Paso 0.3):
- Estática → pedir ip. Para empresa 1 (red
192.168.0.0/16) se aceptan los dos últimos octetos (26.150 → 192.168.26.150). Para otros tenants pedir la IP completa (la red base sale de grupos_empresa.allowed_networks).
- PPPoE → NO pedir IP. Las credenciales
ppp-usuario/ppp-clave son opcionales: si no se dan, el script las autogenera (usuario = pppoe<id>, clave aleatoria) y las reporta para que el técnico las cargue en el CPE. Pedirlas solo si el operador quiere fijarlas a mano.
Opcionales (defaults si no se dan):
- email (vacío). Si se da, dispara email de bienvenida del tenant.
- valor-afiliacion:
0
- standby:
0 (si 1 → email new_not_installed, "esperando instalación")
- cajero:
codex-manual
- requiere-factura-legal:
0. Preguntar SOLO si el contexto sugiere factura electrónica legal. Si es 1, pedir tambien tipo-documento (CC/NIT/CE/PAS/TI) y razon-social (solo si NIT/empresa). Solo captura el dato (FE0), no emite nada.
No preguntar velocidad/valor-plan a mano: salen del --id-plan. No existe ya quote ni activo (campos removidos/deprecados).
Paso 2 — Validar y resolver (todo scopeado por la sucursal)
- Áreas de la sucursal (resuelven
id_client_area → ciudad/departamento):
SELECT id, nombre, tipo_zona, ciudad, departamento
FROM areas WHERE `id-grupos-empresa` = <id-grupos-empresa>;
Mostrar al usuario para que elija. areas NO tiene columna descripcion (es nombre). El área elegida debe pertenecer a esa sucursal (el script lo exige).
- Catálogo de planes activos de la sucursal:
SELECT id, nombre, velocidad_bajada_mbps, precio_mensual, tipo_cliente
FROM planes WHERE `id-grupos-empresa` = <id-grupos-empresa> AND activo = 1;
Mostrar y dejar que el usuario elija el id → ese es --id-plan.
- Estática — resolver/validar la IP:
- PPPoE — sin IP ni tercer octeto. Si el operador fijó
ppp-usuario manual, verificar que no esté tomado:
SELECT a.id, a.cliente, a.apellido FROM afiliado_red r
JOIN afiliados a ON a.id = r.`id-afiliado`
WHERE r.ppp_usuario = '<usuario>' AND (a.eliminar=0 OR a.eliminar IS NULL);
(El autogenerado pppoe<id> es único por construcción; no validar.)
Paso 3 — Mostrar resumen y pedir aprobacion
Antes de ejecutar, siempre muestra el resumen y pide confirmación explícita ("si" / "aprobado" / "ejecuta"). Formato (omitir las líneas que no apliquen al tipo):
- empresa (id-empresa): <N>
- sucursal (id-grupos-empresa): <N — desc> [mostrar siempre que sea multi-sucursal]
- tipo de nodo: estático | PPPoE
- cliente: <nombre>
- apellido: <apellido>
- cedula: <cedula>
- telefono: <telefono>
- mail: <email o "vacio">
- direccion: <direccion>
- id_client_area: <area_id> (<nombre área>)
- ciudad: <resuelta>
- departamento: <resuelto>
- corte: <1-28>
- plan (id-plan): <id> — <nombre> (<velocidad>Mbps, $<precio>, <tipo_cliente>)
- registration-date: <hoy>
- source: codex
- eliminar: 0
- standby: <0|1>
- valorAfiliacion: <n>
- cajero: <cajero>
[Si ESTÁTICO]
- ip: <ip completa>
- id-repeater-subnets-group: <resuelto de la BD>
[Si PPPoE]
- ppp-usuario: <usuario manual, o "autogenerado: pppoe<id>">
- ppp-clave: <manual, o "autogenerada (se reporta al crear)">
[Si requiere factura legal]
- requiere_factura_legal: 1
- tipo_documento: <CC|NIT|CE|PAS|TI>
- razon_social: <si aplica>
Factura inicial:
- estado: <abierta|cerrada>
- periodo: <día 1-20 = mes actual; día 21-31 = mes siguiente — lo calcula el script igual que init-fact-mensual; muestra el nombre del mes que resultará. Override opcional con --periodo=NombreMes>
- notas: Factura-1er mes
- valorf: <precio del plan>
- valorp: <0 si abierta | precio si cerrada>
- saldo: <precio si abierta | 0 si cerrada>
- cerrado: <0 si abierta | 1 si cerrada>
- fecha-pago / fecha-cierre: <NULL si abierta | hoy si cerrada>
El recaudador (id-formato-factura) y el IVA los resuelve el script por empresa — no preguntarlos. No ejecutes hasta aprobación. Si el usuario corrige algo, vuelve a mostrar el resumen.
Período y corte (día 21-31): si das de alta en la ventana de facturación del mes siguiente, la factura inicial queda en el mes siguiente (igual que init-fact-mensual). Eso es intencional: el cliente debe ese saldo pero los crones de corte excluyen el período del mes siguiente, así que no se le corta el servicio al día siguiente; pasa a ser cortable el mes que viene cuando ese período se vuelve exigible. No hace falta hacer nada especial.
Paso 4 — Ejecutar
Base común:
php /var/www/ispexperts/public/tools/add_afiliado.php \
--empresa-id=<n> \
--area-id=<N> \
--nombre="<nombre>" \
--apellido="<apellido>" \
--cedula="<cedula>" \
--direccion="<direccion>" \
--telefono="<telefono>" \
--email="<email o vacio>" \
--id-plan=<N> \
--valor-afiliacion=<n> \
--corte=<1-28> \
--standby=<0|1> \
--cajero="<cajero>" \
--estado-factura=<abierta|cerrada>
Agregar según el caso:
- Estática:
--ip="<ip completa>"
- PPPoE: (nada de IP). Para fijar credenciales manuales:
--ppp-usuario="<u>" --ppp-clave="<c>". Si se omiten, se autogeneran.
- Multi-sucursal (Paso 0.2 con >1 grupo):
--id-grupos-empresa=<sucursal elegida> (alias --sucursal-id=). En mono-sucursal NO pasarlo.
- Factura legal (FE0):
--requiere-factura-legal=1 --tipo-documento=<CC|NIT|CE|PAS|TI> --razon-social="<legal>".
- Identidades de sesión (si el operador las fijó, ver Paso 0):
--id-formato-factura=<N> (a nombre de quién se factura) y --id-recaudador=<N> (a dónde paga / Nequi). Si se omiten, el script usa el default del tenant.
- Sin catálogo (migración/caso histórico):
--allow-no-plan reemplaza a --id-plan (último recurso documentado).
Si el script falla (exit != 0), muestra el stderr completo. No reintentes automáticamente.
Paso 5 — Reportar
Tras éxito (el script imprime JSON), reporta:
- ID del afiliado y de la factura.
- Si PPPoE:
ppp_usuario y ppp_clave del JSON (el técnico los carga en el CPE) y la línea [pppoe] secret ... -> <status> (si el router estaba caído, el alta quedó OK pero hay que re-aplicar el secret).
- Si estática: la línea
[queue] <action> (queue de velocidad en el router borde; best-effort).
mail_status: success / fail / skipped.
- El comando ejecutado (para registro).
Sobre el email
El script dispara el email de bienvenida si --email no está vacío, vía EmailTenant::send, que resuelve template v2 + branding del tenant emisor automáticamente (sin clones por empresa). Plantilla por --standby: 0 → new_installed, 1 → new_not_installed. El microservicio Node de mailer/ debe estar arriba (pm2 status / ps aux | grep mailer/index.js).
Reglas estrictas
- Nunca ejecutes sin aprobación explícita (Paso 3).
- Nunca inventes valores faltantes: pregunta los obligatorios.
- Nunca INSERT directo a
afiliados: usa siempre el script (tiene validaciones críticas, anclaje a sucursal, recaudador, queue/secret, email).
- Nunca asumas tipo estático ni red
192.168: resuelve grupos_empresa.tipo y allowed_networks por sucursal.
- Siempre scopea las queries por
id-grupos-empresa (áreas, planes, segmento RSG): los IDs no son únicos entre tenants — una query sin scope mezcla/filtra entre ISP.
- Multi-sucursal sin sucursal indicada → pregúntala, nunca asumas la primera.
- Segmento de IP inexistente en la sucursal → detente, no llames al script.