Documento: explicación técnica de la aplicación (arquitectura, algoritmos, modelo de datos, seguridad y mapa de fuentes). Versión del producto:
0.3.1· Fecha: 2026-09-22 · Autor: Rocky (IA DevOps) + Javier Páez
Este documento es el white sheet técnico de la
aplicación. Complementa a: 07 — Manual
DevOps, 08 — Versiones y 09 — Stack tecnológico. El mapa de
fuentes (§11) apunta al código real en el repositorio
(git).
Sistema empresarial de control de asistencia georeferenciado para cumplimiento laboral mexicano (LFT / NOM-037 / INAI). El empleado registra su entrada/salida desde una app móvil que valida: (1) quién (biometría local + reconocimiento facial on-device), (2) dónde (GPS + geocerca PostGIS), (3) cuándo (hora firmada por el servidor, no por el dispositivo) y (4) que el dispositivo no está manipulado (anti-spoofing de mock-location / root / jailbreak).
Cada registro es un evento inmutable encadenado con
HMAC-SHA256 (tamper-evidence), apto como evidencia legal. La
administración (RR.HH.) opera un panel web con mapa, geocercas,
empleados, turnos y aprobaciones. La salida se sincroniza con
Odoo 18 (hr.attendance) mediante un outbox
idempotente.
| Área | Funcionalidad | Estado |
|---|---|---|
| Autenticación | Login JWT + registro autoservicio (email + contraseña) | ✅ v0.3.1 |
| Asistencia | Check-in / check-out con Entrada/Salida explícita | ✅ v0.3.0 |
| Biometría | local_auth (huella/rostro) + facial on-device (ML
Kit) |
✅ v0.2.0 |
| Anti-spoofing | mock location + root/jailbreak + hora NTP | ✅ v0.1.0 |
| Geocercas | PostGIS ST_DWithin, perfiles sitio/remoto |
✅ v0.1.0 |
| Historial | GET /attendance/me + badge “pendiente” |
✅ v0.2.0 / v0.3.1 |
| Aprobaciones | Flujo administrativo fuera de zona/horario | ✅ v0.3.1 |
| Panel web | Mapa + zonas + empleados + asignaciones + aprobaciones | ✅ v0.3.1 |
| Nómina | Horas ordinarias/extra (doble/triple), retardos (LFT) | ✅ (lógica pura) |
| Odoo | Outbox odoo_sync → hr.attendance
(XML-RPC) |
🟡 plan (pipeline codificado) |
| Offline | Cola cifrada + sync idempotente | 🟡 parcial |
Ver diagrama PNG docs/diagrams/01-arquitectura.png.
Capas:
┌─────────────────────────────────────────────────────────────┐
│ Clientes: App Flutter (Android/iOS) · Panel web React │
└───────────────┬─────────────────────────┬───────────────────┘
│ HTTPS + JWT (Bearer) │ HTTPS + JWT
▼ ▼
┌───────────────────────────────────────────┐
│ Cloudflare CDN → nginx (Linode) │
└────────────────────┬──────────────────────┘
▼
┌───────────────────────────────────────────┐
│ Backend FastAPI (uvicorn, async) │
│ auth · attendance · sites · admin ... │
│ geofence · payroll · odoo_client · hash │
└──────┬──────────────────────────┬─────────┘
│ asyncpg (SQLAlchemy 2) │ XML-RPC (odoorpc)
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ PostgreSQL 15+PostGIS│ │ Odoo 18 (hr.attend.) │
└──────────────────────┘ └──────────────────────┘
Principios de diseño: 1. Backend async
end-to-end (FastAPI + SQLAlchemy 2 asyncio + asyncpg). 2.
Eventos append-only: la asistencia no se edita ni
borra; se corrige con eventos nuevos + auditoría. 3. Autoridad
de tiempo en el servidor (NTP), nunca el reloj del dispositivo.
4. PII cifrada en reposo (AES-256-GCM vía Fernet). 5.
Separación por capas (api/ →
services/ → models/ → core/).
Ver db/000_schema.sql
y diagrama docs/diagrams/03-er-diagrama.png.
| Tabla | Rol |
|---|---|
companies |
Multi-tenant (empresa) |
users |
Empleados/usuarios (profile_type: site /
remote) |
sites |
Centros de trabajo (geocerca:
geom GEOGRAPHY(Point,4326) + radius_m) |
employee_site_assignments |
Asignación empleado ↔︎ sitio
(valid_from/valid_until) |
shifts |
Turnos (jornada_type:
diurna/nocturna/mixta) |
attendance_events |
Núcleo, inmutable + cadena de hash |
attendance_approvals |
Aprobaciones (mutable; refiere al evento) |
attendance_audit_log |
Correcciones legales (auditoría) |
consents |
Consentimientos INAI |
devices / push_tokens |
Dispositivos y tokens push |
alerts |
Alertas (fraude, salida faltante, fuera de geocerca) |
odoo_sync |
Outbox de sincronización con Odoo |
backend/app/models/models.py)UserRole: employee,
adminProfileType: site,
remoteEventType: check_in,
check_outEventSource: gps, manual,
adminAuthMethod: standard,
biometric, faceGeofenceStatus: inside,
outside, not_requiredApprovalStatus: pending,
approved, rejectedJornadaType: diurna,
nocturna, mixtaOdooSyncStatus: pending,
sent, errordb/001_immutability.sql)trg_events_immutable bloquea
UPDATE/DELETE sobre
attendance_events.REVOKE de privilegios de escritura sobre la tabla.Ver diagrama docs/diagrams/02-flujo-checkin.png.
mobile/lib/features/attendance/) — el usuario elige
Entrada o Salida.biometric_service.dart) —
local_auth (huella/rostro).mock_location_detector.dart,
security_service.dart) — mock/root/jailbreak.geolocation_service.dart) —
geolocator con accuracy_m./attendance/check-in
(backend/app/api/attendance.py +
services/attendance.py):
services/geofence.py →
(GeofenceStatus, site_id).pending.odoo_sync (pendiente).geofence_status,
server_time, approval_status.tasks/scheduler.py) sincroniza odoo_sync →
Odoo.services/geofence.py)remote → not_required (guarda
coordenadas, sin validación de radio).site sin coordenadas / sin sitio vigente / fuera
del radio → outside (pendiente de aprobación).site dentro del radio → inside
(válido).La consulta usa ST_DWithin sobre geography
(distancias en metros), tomando el sitio primario
vigente (is_primary DESC + valid_until).
core/security.py)PyJWT): access token (sub, type,
iat, exp) + refresh token.argon2-cffi).
Un hash malformado nunca autentica.core/crypto.py)cryptography).
Clave en PII_ENC_KEY.BYTEA. Desencriptado fallido →
ValueError (nunca leak).core/ntp.py)server_time_ms con HMAC-SHA256
(SERVER_TIME_SECRET).offset; el reloj
del dispositivo es solo un claim.MAX_TIME_OFFSET_MS descarta dispositivos con reloj muy
desviado.core/hash_chain.py)hash = HMAC_SHA256(ATTENDANCE_HASH_SECRET, prev_hash || canonical_row).canonical_row es determinista (campos ordenados:
coords, flags, server_time, idempotency_key,
etc.).prev_hash del evento N+1 ya no coincide).flutter_jailbreak_detection (root/jailbreak).mock_location_detector.dart).google_mlkit_face_detection (sin subir biometría al
servidor).MAX_GPS_ACCURACY_M descarta fijaciones GPS imprecisas
(posible spoofing).services/payroll_rules.pyFunciones puras (testeables, sin BD) que codifican la ley federal:
classify_jornada) por proporción de franja nocturna.compute_retardo_minutes):
diferencia entre entrada real y hora programada.compute_workday):
ordinarias, extra, retardos, pausas.La autoridad del tipo de jornada es
shifts.jornada_type;classify_jornadaes heurística de respaldo. Verbackend/tests/test_payroll_rules.py.
Ver docs/05-odoo-integration.md
y PLAN_INTEGRACION_ODOO.md.
odoo_client.py envuelve
odoorpc (XML-RPC) contra hr.attendance.odoo_sync: cada evento genera
una fila pending; el worker la marca
sent/error de forma idempotente (nunca duplica
un check-in en Odoo).users.odoo_employee_id
mapea al hr.employee de Odoo.odoo_sync), campos custom x_* en Odoo,
usuario API + credenciales .env,
enable_scheduler=True, y el sync de entrada de
empleados.mobile/lib/core/storage/offline_store.dart): guarda
check-ins locales cuando no hay red
(connectivity_plus).mobile/lib/features/sync/sync_service.dart): reenvía con
idempotency_key (UUID) para evitar duplicados.site/remote, geocercas, consentimientos.consents explícitos,
PII cifrada en reposo, datos mínimos.docs/04-nom037-y-lft.md.backend/| Componente | Archivo(s) |
|---|---|
| Punto de entrada / routers / CORS / panel estático | app/main.py |
Configuración (Settings, .env) |
app/core/config.py, .env.example |
| Conexión BD (async engine) | app/db.py |
| Modelos / enums | app/models/models.py |
| Esquemas Pydantic | app/schemas/ |
| Auth (login/refresh/register) | app/api/auth.py |
| Check-in/out | app/api/attendance.py,
app/services/attendance.py |
| Geocercas | app/services/geofence.py |
| Nómina LFT | app/services/payroll_rules.py |
| Odoo client | app/services/odoo_client.py |
| Alertas / notificaciones | app/services/alerts.py,
app/services/notifications.py |
| Cadena de hash | app/core/hash_chain.py |
| Tiempo NTP | app/core/ntp.py |
| Cifrado PII | app/core/crypto.py |
| JWT + Argon2id | app/core/security.py |
| Admin (asignaciones, aprobaciones, sitios, empleados) | app/api/admin.py |
| Reportes (timesheet, eventos geo) | app/api/reports.py |
| Scheduler / worker | app/tasks/scheduler.py |
| Seed demo | scripts/seed_demo.py,
scripts/seed_attendance_month.py |
| Tests | tests/test_hash_chain.py,
tests/test_payroll_rules.py,
tests/test_time_api.py |
db/| Archivo | Contenido |
|---|---|
000_schema.sql |
Esquema + enums + PostGIS |
001_immutability.sql |
Trigger append-only + REVOKE |
002_seed.sql |
Datos demo (10 empleados, 3 sitios) |
003_approvals.sql |
Tabla attendance_approvals + backfill |
mobile/lib/| Componente | Archivo(s) |
|---|---|
| Entrypoint / tema / rutas | main.dart, app.dart |
| Config base URL | core/config.dart |
| Cliente HTTP + auto-refresh JWT | core/network/api_client.dart |
| Secure storage resiliente | core/storage/secure_storage.dart |
| Cola offline | core/storage/offline_store.dart |
| Tiempo NTP (cliente) | core/time/ntp_time.dart |
| Auth (login/registro) | features/auth/auth_service.dart |
| Check-in service | features/attendance/services/attendance_checkin_service.dart |
| Biometría | features/attendance/services/biometric_service.dart |
| Facial | features/attendance/services/face_verification_service.dart |
| Geolocalización | features/attendance/services/geolocation_service.dart |
| Mock/root detector | features/attendance/services/mock_location_detector.dart,
security_service.dart |
| Historial / badge | features/attendance/history_screen.dart |
| Sync | features/sync/sync_service.dart |
| Mapa geocerca | features/maps/geofence_view.dart |
| MainActivity (host Android) | android/app/src/main/kotlin/.../MainActivity.kt |
web/| Componente | Archivo(s) |
|---|---|
| Entrypoint | src/main.tsx, src/App.tsx |
| Cliente API | src/api.ts |
| Config env | .env.development, .env.production |
| Artefacto | Ubicación |
|---|---|
| Compose (PostGIS/Redis local) | docker-compose.yml |
| Diagramas | docs/diagrams/*.png (+ generador
_gen_diagrams.py) |
| Checksums APK | entregas/asistencia-geo/SHA256SUMS.txt |
Detalle operativo completo en docs/07-devops-manual.md y
diagrama docs/diagrams/04-despliegue.png.
172.238.169.174 →
Cloudflare → nginx → systemd ia-asistencia.service (uvicorn
:8095) → Docker asistencia-postgis
(:5433) → Odoo :8069./opt/ia-intt/html/downloads/asistencia/ (binarios
no van a git; solo el checksum
SHA256SUMS.txt).notifications.py es un stub.allow_origins=["*"] →
dominios concretos).ios/ está parcial).