Firma certificada (UANATACA)
Cómo firma platform con valor probatorio (SigningService agnóstico del proveedor), flujo async con UANATACA, webhooks blindados e idempotencia.
Axel firma contratos con firma certificada por un PSCE acreditado en El Salvador (UANATACA, acreditado por la UFE/MINEC): la única con pleno valor probatorio en juicio laboral (Art. 24 de la Ley de Firma Electrónica). El resultado es un PDF PAdES nivel T, y el ciclo cierra 100% digital con el registro MTPS ante la DGT.
SigningService: frontera agnóstica del proveedor
Toda la dependencia del proveedor de firma vive detrás de una interface. El
producto le habla a SigningService, no a UANATACA.
SigningService: openSession(), sign($contractId, $signers),
jobStatus($jobId), closeSession(). Espejo de la API Signbox, pero sin
mencionar al proveedor.
FakeSigningService (ciclo simulado, para #1/#2 y tests) y
UanatacaSigningService (real). El bind se togglea por env, sin cambiar
código de consumo.
Naming agnóstico del proveedor. Las columnas son psce_session_id /
provider_job_id + un enum provider, no uanataca_*. Aunque la decisión cerró
con UANATACA, el naming neutral se mantiene: un PSCE acreditado puede volver a
fallar, y migrar de proveedor no debe forzar a renombrar columnas y rutas con
firmas vivas persistidas. UanatacaSigningService normaliza el payload del
proveedor a estas columnas neutrales.
Flujo de firma async
El flujo del PSCE es asíncrono por naturaleza: open → sign → poll status → close. Eso tolera jitter de red sin requerir respuesta síncrona instantánea.
- openSessionabre sesión + OTP al firmante
- signpor contrato, dispara el job PAdES
- poll statuscreated → signing → completed
- closeSessionrecibo de sesión
El job PAdES es asíncrono: se dispara y se consulta por estado, no se espera en línea. El resultado es un PAdES nivel T.
Son dos modos con dos Optimizers Docker distintos:
| Modo | Optimizer | Pantalla |
|---|---|---|
| Batch (firma múltiple del patrono, un solo OTP) | SignBox Optimizer | PatronoBatch |
| Individual (empleado sin cuenta, OTP one-shot) | One-Shot Optimizer + Redis | EmpleadoSign |
El OTP lo genera, envía y valida el SDK del PSCE (por SMS, por defecto), no
Axel: el segundo factor probatorio lo controla el certificador acreditado. El lado
de Axel es solo el enlace firmado (temporarySignedRoute) que enruta al empleado
al flujo del PSCE. El empleado firma sin cuenta.
Webhooks blindados
El Optimizer requiere que platform exponga webhooks agnósticos del proveedor:
/api/signing/callback/document (el PDF firmado) y /api/signing/callback/status
(logs de estado). Tres requisitos que no se dan por asumidos:
El Cloudflare Tunnel autentica el origen de la conexión, pero no el contenido. Se suma HMAC del cuerpo con secreto del PSCE: un job mal enrutado no debe poder marcar un documento como firmado.
El PDF entrante se re-valida como PAdES antes de persistirlo. Un PDF que no valida no se guarda como firmado, aunque el proveedor diga “completed”.
Durabilidad del PDF firmado
El Optimizer es un SPOF stateful (Redis) y corre en otra máquina que el PITR de Laravel Cloud no cubre. El orden de operaciones se blinda contra una caída a mitad:
No se borra del Optimizer hasta confirmar por HEAD/checksum que el PDF está
en R2 (reintento con backoff antes de cualquier delete). Además el PDF tiene un
segundo hogar: también llega a platform vía el callback de documento.
signing_jobs persiste el status por-contrato en Postgres (con PITR), así que un
batch parcial se reanuda sin re-firmar lo ya firmado.
Modelo de datos
signing_sessions id, tenant_id, provider, psce_session_id, correlation_id,
status, opened_by, timestamps
signing_jobs id, session_id, contract_id, provider_job_id, request_id,
status (created|signing|completed|failed), pades_level, timestamps
→ unique (tenant_id, provider_job_id) · índice (tenant_id, status)
contracts
signing_level enum(simple|certificada), default certificada
signed_pdf_path storage path del PDF firmado
correlation_id (UUID propio de Axel) hila el flujo a través de las tres capas
(Laravel Cloud ↔ Optimizer ↔ PSCE) para depuración.
Registro MTPS
Tras firma certificada, el paquete MTPS se genera con la firma real incorporada al PDF (nota de remisión + checklist). “Marcar como presentado” requiere el checklist completo.
La DGT acepta el documento con firma certificada (PAdES/UANATACA) en “Oportunidades Empresa”. El “100% digital” está despejado de extremo a extremo, sin condicional.
Contract test de SigningService
El gate de “hecho” incluye un contract test que corre la misma suite contra
FakeSigningService (#1/#2) y el sandbox real del PSCE (#3): un reintento
idempotente no duplica, el estado failed se propaga, y un timeout no deja el
contrato en estado ambiguo. Ver Testing y verificación.