Cierra el ciclo de la factura: construcción del comprobante, sellado con el CSD de la
empresa emisora y transmisión al PAC.
- cfdi_builder: XML 4.0 de ingreso en el orden de atributos del XSD, del que depende la
cadena original y con ella el sello. Todo el dinero con Decimal.
- sealer: cadena original vía el XSLT oficial del SAT y firma con la llave del CSD.
- pac_comercio_digital: cliente de timbrarV5. Conserva el código y el saldo de folios que
el legado leía en una variable que descartaba (CFDI.cs:19324-19336).
- csd_service y core/crypto: CSD por empresa, con la contraseña cifrada en la base. Antes
el certificado había que dejarlo a mano en el almacenamiento y su contraseña era una
variable de entorno global, lo que no funciona con varias empresas emisoras.
- Cada intento —también los rechazados— guarda el XML que se transmitió y el que contestó
el PAC: sin ese par no hay forma de reconstruir un rechazo cuando termina la petición.
La declaración XML se escribe a mano con comillas dobles. lxml la emite con comillas
simples, que es XML válido, pero Comercio Digital compara la cadena literal version="1.0"
y responde 642 "la versión del XML no es 1.0".
El modo (pruebas o producción) sale de invoices.stamping_mode y no se puede pasar por la
API: es lo único que separa un timbre de prueba de un CFDI con validez fiscal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
list_users/stats llamaban al Hub (users-with-info) con el Bearer de la app, que con
el patrón SIWEB es la sesión local (HS256) → el Hub la rechaza (401) → 0 usuarios.
Nuevo core.hub_token.get_hub_access_token: toma el token KC de la sesión (valkey vía
crm_sid) y lo refresca si está por expirar. list_users y stats lo usan para el Hub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Las compañías (a76.company) se gestionan en el CRM, ligadas a un tenant del
Workspace. Cambios:
- my-companies: por MEMBRESÍA (user_tenants ∪ user_company_roles), así el hub_admin
sin tenant en el token ve las compañías que creó/se le asignaron; para usuarios
con tenant, autocrea una por defecto en el primer acceso.
- POST /auth/companies: da de alta una compañía bajo un tenant + asigna al usuario.
GET /auth/assignable-tenants: tenants elegibles (hub_admin: todos; usuario: el suyo).
- security.validate_access_to_resource: si el token no trae tenant (hub_admin),
resuelve el tenant desde la compañía activa (a76.company.tenant_id) → puede operar
por compañía seleccionada.
- set-active: fija sso_tenant_id/pub con el tenant de la compañía (override).
- Pantalla "Compañías" (nav) para crear/listar/seleccionar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El LicenseValidationMiddleware reenviaba el Bearer al Hub /verify-license, pero con
el patrón SIWEB el Bearer es un JWT HS256 local que el Hub no entiende → 401 →
silent-refresh infinito → toast "Sesión expirada" en cada página.
Ahora, para una sesión local válida, la licencia se valida con el token KC guardado
en valkey (refrescándolo si está vencido) y se cachea por tenant (TTL 10 min). Si el
Hub no es concluyente (su refresh falla), se permite el paso (la sesión se emitió tras
un login válido) evitando el bucle; los resultados concluyentes sí se cachean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Desacopla la sesión de la app del access token de Keycloak (60s). Tras el SSO/
refresh, el backend emite un JWT de sesión local (HS256) con vida por inactividad
(SESSION_IDLE_MINUTES, cap SESSION_MAX_HOURS) y guarda los tokens KC en valkey.
La app usa esa sesión local como bearer (cookie access_token); el token KC vive en
kc_access_token solo para llamadas directas al Hub. Así el refresh KC contra el Hub
solo se intenta al expirar la sesión local, no cada ~60s → se elimina el bucle
causado por el bug "Token is not active" del relay.
Se RESPETA la revocación central de Keycloak: si el Hub rechaza el refresh, la
sesión termina (401). Sin re-emisión de fallback → sin bypass de revocación.
Todo detrás de SESSION_STORE_ENABLED (default False = comportamiento idéntico).
Backend: core/local_session.py, core/session_store.py (valkey), verify_token acepta
la sesión local, refresh la emite/actualiza; DTOs con session_token/session_id;
test unitario de local_session.
Frontend: access_token=sesión local, kc_access_token para el Hub; sso/refresh/
silent-refresh/switch-tenant y pantallas de workspace cableadas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>