Carnet Equidad
Plugin de wordpress para generar carnets de usuarios de polizas de seguros
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/wilcastel/carnet-eq/archive/refs/heads/master.zipPlugin de WordPress: una página pública donde una persona escribe su número de cédula, el plugin consulta el servicio Api Data Carnet de La Equidad del lado del servidor, y la persona elige cuál de sus registros de póliza quiere.
Alcance del incremento 1. La generación del carnet en PDF descargable a partir del registro elegido no forma parte de este incremento.
Qué hace
- El shortcode
[carnet_equidad_form]muestra un formulario público con un solo campo (cédula). Un script pequeño en JavaScript sin dependencias y un CSS mínimo se cargan únicamente en las páginas que contienen el shortcode. - Ruta REST
POST /wp-json/carnet/v1/consulta, cuerpo{ "cedula": "..." }:- Limpia la cédula dejando solo dígitos; exige entre 6 y 11 dígitos (
400en caso contrario). - Llama a
ApiDataCarnetClienty transforma el resultado:- encontrado →
200 { "status": "found", "asegurado": "<NOMBRE_ASEGURADO del primer registro>", "opciones": [ { "id", "poliza", "orden", "certificado", "sucursal", "vigencia_desde", "vigencia_hasta" } ] } - no encontrado (
404del servicio) →200 { "status": "not_found" } - demasiadas peticiones →
429 { "status": "rate_limited", "message": "<mensaje>" }, con la cabeceraRetry-Afteren segundos - fallo del servicio / autenticación / configuración →
502 { "status": "error", "message": "<mensaje genérico>" }
- encontrado →
- Todas las respuestas incluyen ahora un campo
"ref": el UUID v4 de la petición, que también es la clave de las filas de auditoría generadas para esa llamada (ver Auditoría más abajo). - La respuesta nunca incluye datos del beneficiario ni el campo
PRIMA. permission_callbackes__return_true(público) por ahora. El cliente JavaScript igual envía el noncewp_rest. Las peticiones están limitadas por IP (ver Límite de peticiones más abajo). PENDIENTE: CAPTCHA antes de producción.
- Limpia la cédula dejando solo dígitos; exige entre 6 y 11 dígitos (
- El JavaScript del front-end envía la petición con
fetchy representa los estadosloading/not_found/error/found. El estadofoundmuestra las opciones como una lista de radios (Póliza / Orden / Vigencia). El botón "Continuar" por ahora solo vuelca la opción elegida en un bloque de resumen y enconsole.log— marcado con// TODO: next increment -> call /pdf endpoint.
Límite de peticiones (rate limiting)
POST /wp-json/carnet/v1/consulta aplica un límite por IP con algoritmo de
ventana fija: se cuenta cuántas peticiones llegan desde una IP dentro de una
ventana de tiempo; al superar el tope, las siguientes se rechazan con 429
(cuerpo { "status": "rate_limited", ... } y cabecera Retry-After en segundos)
hasta que la ventana se reinicia. El contador se guarda en un transient cuya
clave es un hash SHA-256 truncado de la IP — la tabla de opciones nunca
almacena una IP en claro. La comprobación ocurre antes de validar la cédula y
antes de cualquier llamada al servicio upstream.
Valores por defecto: 10 peticiones cada 600 segundos (10 minutos).
X-Forwarded-For no se usa por defecto: en una conexión directa lo controla
quien hace la petición. En despliegues detrás de proxy / CDN, sobrescribe la IP
con el filtro carnet_equidad_client_ip.
Filtros disponibles
| Filtro | Valor por defecto | Forma esperada del retorno |
|---|---|---|
carnet_equidad_rate_limit |
['limit' => 10, 'window' => 600] |
Array ['limit' => int, 'window' => int]. Se lee a la defensiva: ambos se convierten a int y se limitan a >= 1; un retorno malformado (no-array) vuelve a los valores por defecto. |
carnet_equidad_client_ip |
$_SERVER['REMOTE_ADDR'] (o '') |
string con la IP del cliente. Se pasa por sanitize_text_field(); si queda vacía se usa 'unknown'. |
carnet_equidad_cedula_length |
['min' => 6, 'max' => 11] |
Array ['min' => int, 'max' => int]. Se lee a la defensiva: ambos se convierten a int; si min < 1 se trata como 1 y si max < min se trata como min; un retorno malformado vuelve a los valores por defecto. |
Ejemplo — subir el límite y confiar en la IP que reenvía Nginx:
add_filter('carnet_equidad_rate_limit', fn () => ['limit' => 30, 'window' => 600]);
add_filter('carnet_equidad_client_ip', fn () => $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '');
Auditoría
Para cumplir con Habeas Data / Ley 1581, el plugin registra un evento
estructurado por acción relevante en una tabla propia
{$wpdb->prefix}carnet_audit (por defecto wp_carnet_audit). Una fila por
evento.
Qué se registra
| Columna | Tipo | Contenido |
|---|---|---|
id |
BIGINT UNSIGNED |
Clave primaria autoincremental |
request_id |
CHAR(36) |
UUID v4, uno por petición HTTP (es el campo ref de la respuesta REST) |
created_at |
DATETIME |
Fecha/hora en UTC |
ip |
VARCHAR(45) |
La misma IP que resuelve el filtro carnet_equidad_client_ip, tal cual |
cedula |
VARCHAR(40) |
El documento consultado, completo (es la clave de auditoría según §6). Si el formato es inválido se guarda el valor crudo enviado, truncado a 40 caracteres |
cod_pla |
VARCHAR(12) |
Código de plan (por ahora siempre "1821") |
result |
VARCHAR(20) |
found, not_found, error, invalid o rate_limited (nulo en eventos que no son consultas) |
api_http |
SMALLINT UNSIGNED |
Código HTTP del upstream cuando se conoce (200 / 404), nulo en otro caso |
event_type |
VARCHAR(20) |
query, token_refresh o auth_error |
detail |
VARCHAR(255) |
Texto corto y seguro (p. ej. "AuthException after retry"). Nunca un payload completo |
poliza, orden, file_hash |
VARCHAR / CHAR |
Reservadas para un incremento posterior; se crean nulas y no se pueblan todavía |
Índices: KEY sobre created_at y sobre request_id.
Eventos actuales:
query— una consulta terminada (o rechazada antes del upstream): se registra para cada desenlace (found/not_found/invalid/rate_limited/error). El casorate_limitedse registra antes de devolver el429.token_refresh— el token del upstream se refrescó tras un401.auth_error— la autenticación siguió fallando tras el refresco (§6: "errores 401 y refrescos de token"). Comparte elrequest_idde la consulta que lo disparó.
Qué NO se registra, nunca
Nombres de asegurados o beneficiarios, el payload completo del upstream, el token
bearer y las credenciales de la API. No existen columnas para esos datos, y
el objeto AuditEvent que circula por el código tampoco los transporta. Un fallo
al escribir la auditoría se registra con error_log() y nunca rompe la
respuesta al usuario.
Retención y limpieza
Retención por defecto: 180 días. Un evento de WP-Cron diario
(carnet_equidad_prune_audit, programado en la activación) borra las filas más
antiguas. El valor es configurable con el filtro
carnet_equidad_audit_retention_days (se lee a la defensiva como int); un valor
<= 0 se fuerza a un mínimo de 1 día para no vaciar nunca la tabla entera.
La tabla se crea en la activación (dbDelta()). En sitios que ya estaban activos
cuando cambia el esquema, un guardián en admin_init (comparado contra la opción
carnet_equidad_db_version) vuelve a aplicar la migración sin necesidad de
reactivar el plugin. Se conserva en la desactivación y solo se elimina
(DROP TABLE IF EXISTS) en la desinstalación.
Visor (acceso restringido)
Herramientas → "Carnet — Auditoría" (add_management_page, capacidad
manage_options): tabla de solo lectura con los ~100 eventos más recientes
(ORDER BY created_at DESC LIMIT 100). Sin filtros ni paginación en este
incremento. Todo el contenido se escapa con esc_html().
Arquitectura
Distribución orientada a screaming / hexagonal bajo src/:
| Ruta | Responsabilidad |
|---|---|
src/Validation/CedulaValidator.php |
Limpia y valida la cédula (puro) |
src/Http/HttpTransport.php |
Interfaz de transporte (post()) — punto de inyección |
src/Http/WpHttpTransport.php |
Implementación con wp_remote_post |
src/Http/HttpResponse.php |
Objeto de valor para la respuesta |
src/Http/TransportException.php |
Fallo a nivel de red |
src/Api/ApiDataCarnetClient.php |
Orquesta token + dataAsegurado (puro) |
src/Api/ClientConfig.php |
Configuración desde constantes; los getters lanzan excepción si faltan |
src/Api/TokenStore.php |
Interfaz de caché del token — punto de inyección |
src/Api/WpTransientTokenStore.php |
Implementación basada en transients |
src/Api/PolicyOptionMapper.php |
Registro crudo → forma recortada opciones (puro) |
src/Api/ApiClientFactory.php |
Ensambla el cliente con sus colaboradores de WP |
src/Api/ClientEventListener.php |
Interfaz observadora del cliente (tokenRefreshed() / authRetryFailed()) |
src/Api/NullClientEventListener.php |
Implementación no-op (4.º argumento por defecto del cliente) |
src/Api/Exception/* |
ConfigException, NotFoundException, AuthException, UpstreamException |
src/RateLimit/RateLimiter.php |
Ventana fija por clave (puro, sin WordPress) |
src/RateLimit/RateLimitResult.php |
Objeto de valor: allowed / remaining / retryAfter |
src/RateLimit/RateStore.php |
Interfaz de caché del contador — punto de inyección |
src/RateLimit/WpTransientRateStore.php |
Implementación con transients (clave = hash de la IP) |
src/Audit/AuditEvent.php |
Objeto de valor del evento de auditoría (puro; sin nombres/token/payload) |
src/Audit/AuditStore.php |
Interfaz de persistencia (insert() / deleteOlderThan()) — punto de inyección |
src/Audit/AuditLogger.php |
Mapea el evento a fila y purga por retención (puro, sin WordPress) |
src/Audit/AuditClientEventListener.php |
ClientEventListener que registra token_refresh / auth_error con el contexto de la petición |
src/Audit/WpdbAuditStore.php |
Implementación con $wpdb sobre {$wpdb->prefix}carnet_audit |
src/Audit/AuditSchema.php |
dbDelta() de la tabla de auditoría (activación) |
src/Admin/AuditPage.php |
Visor de solo lectura en Herramientas (manage_options) |
src/Rest/ConsultaController.php |
Registra y atiende la ruta REST (rate limit + auditoría + campo ref) |
src/Frontend/ShortcodeRenderer.php |
Shortcode + encolado condicional de assets |
src/Plugin.php |
Raíz de composición (hooks, activación, cron de purga) |
ApiDataCarnetClient, CedulaValidator, PolicyOptionMapper, RateLimiter,
AuditEvent, AuditLogger y AuditClientEventListener no dependen de
WordPress, y eso es lo que permite probarlos de forma unitaria.
WpdbAuditStore, AuditSchema, AuditPage y el cableado de auditoría dentro de
ConsultaController no tienen prueba unitaria (mismo criterio que
WpTransientTokenStore y el propio controlador: dependen de WordPress / $wpdb).
Comportamiento del cliente del servicio
getToken()→POST {base}/api/v1/validarTokencon{ "user", "password" }. La clave del token en el JSON es literalmente"token: "(con espacio y dos puntos al final); se acepta una clave limpia"token"como alternativa. El token se cachea en el transientcarnet_equidad_api_tokencon un TTL conservador de 10 minutos (la duración real no está confirmada — pregunta abierta #5).getDataAsegurado(string $cedula): array→POST {base}/api/v1/dataAseguradoconAuthorization: Bearer <token>y{ "consumer": "Linktic", "doc_aseg": <cedula>, "cod_pla": "1821" }.200→ devuelve el array de registros crudos ya decodificado.404→ lanzaNotFoundException(centinela elegido; no devuelvenull).401→ borra el token cacheado, se vuelve a autenticar y reintenta una vez; un segundo401lanzaAuthException.- cualquier otro código fuera de 2xx / fallo de transporte → lanza
UpstreamException.
consumer = "Linktic"ycod_pla = "1821"son constantes de clase, todavía pendientes de confirmación formal (preguntas abiertas #8 y #10).
Constantes requeridas en wp-config.php
Agrégalas antes de la línea /* That's all, stop editing! */:
define( 'CARNET_API_USER', '0017-LIN-0033' ); // requerida — credencial real (sin el prefijo "user")
define( 'CARNET_API_PASSWORD', 'tu-clave-aqui' ); // requerida — credencial real
define( 'CARNET_API_BASE_URL', 'http://192.168.243.194:9050' ); // opcional — este es el valor por defecto
CARNET_API_BASE_URL es opcional; si se omite, el cliente usa
http://192.168.243.194:9050 (IP privada, accesible solo a través de la VPN).
CARNET_API_USER / CARNET_API_PASSWORD son obligatorias — el cliente lanza
ConfigException (mapeada a un error genérico 502) cuando falta alguna.
Instalación
cd wp-content/plugins/carnet-equidad
composer install --no-dev # producción
Luego activa "Carnet Equidad" en wp-admin y coloca [carnet_equidad_form] en una
página.
Ejecución de las pruebas
cd wp-content/plugins/carnet-equidad
composer install # incluye phpunit (dev)
vendor/bin/phpunit
La suite es PHP puro — no requiere arrancar WordPress. Cubre:
CedulaValidator— limpieza y la regla de 6 a 11 dígitos.ApiDataCarnetClientcon unHttpTransportfalso + unTokenStorefalso: camino feliz, cacheo y reutilización del token, la clave rara"token: ", centinela de404, un401→ re-autenticación + reintento,401repetido →AuthException,5xxy fallo de transporte →UpstreamException.PolicyOptionMapper— forma recortada, campos de beneficiario /PRIMAdescartados, normalización de fecha"2026-02-01T00:00:00"→"2026-02-01".ApiDataCarnetClient(auditoría) — un spy deClientEventListenercomprueba quetokenRefreshed()se dispara en la ruta de reintento tras401y no en el camino feliz con caché fría, y queauthRetryFailed()se dispara cuando un segundo401desemboca enAuthException.AuditEvent/AuditLogger/AuditClientEventListenercon unFakeAuditStoreen memoria: mapeo de cada evento a fila (con la redacción — sin claves de nombre/token/payload),prune()borra solo lo anterior al corte y devuelve el conteo, yretentionDays <= 0se fuerza a>= 1día.
Pendientes / preguntas abiertas
De Documents/carnet-equidad/acercamiento-proyecto.md §9. Estas afectan a este
incremento:
- #11 / #13 — formato de la cédula y ceros a la izquierda. Por ahora quitamos todo lo que no sea dígito y conservamos los ceros a la izquierda tal como se escriben. Sin confirmar si el servicio espera el documento "tal cual" sin dígito de verificación, ni si los ceros a la izquierda importan.
- #14 — el rango de longitud 6–11. Tomado del documento de trabajo; sin confirmar que ningún documento real quede fuera de ese rango.
- #15 — por qué
dataAseguradodevuelve un array. Exponemos cada registro como una opción seleccionable; los casos reales con múltiples registros (varias pólizas, renovaciones, varias instituciones) aún no están confirmados. - #5 — TTL del token. No hay duración documentada ni endpoint de refresco, así
que el TTL del transient es una estimación conservadora (10 min) y además lo
refrescamos de forma reactiva ante un
401. - #21 — formato de fecha / zona horaria.
"2026-02-01T00:00:00"se trunca a la parte de fecha; se desconoce la zona horaria.
Bloquea el SIGUIENTE incremento (carnet en PDF)
- #23 / #24 / #25 — cuál de los dos diseños del carnet es el definitivo, en qué formato (PDF vectorial vs imagen vs AI/PSD), dimensiones finales y si lleva reverso.
- #16 — cuando hay varios registros, cuál debe usar el carnet.
- #17 — de dónde salen los campos "Tomador" y "V/r asegurado por gastos médicos" (no vienen en la respuesta de la API).
- #26 / #27 / #28 — foto, código QR / de verificación, y textos legales / firmas / logos obligatorios en el carnet.
- Elección de la librería PDF (FPDI+TCPDF vs mPDF), a la espera del formato final de la plantilla.