25 KiB
Anexo76 - Resumen de Arquitectura Técnica
📋 Índice
- Visión General
- Stack Tecnológico
- Arquitectura del Sistema
- Arquitectura de Schemas y Módulos
- Estructura del Proyecto
- Flujos Principales
- Seguridad
- Base de Datos
- API Reference
Visión General
Anexo76 es una aplicación SaaS multi-tenant para gestión de comercio exterior en México, enfocada en cumplir con los Anexos 24, 31 y 22 del SAT.
Objetivos de Negocio
- Gestión de inventarios para maquilas e IMMEX
- Control de pedimentos aduanales
- Manejo de facturas de importación/exportación
- Cumplimiento normativo SAT
- Licenciamiento flexible por planes
Stack Tecnológico
Backend
- Framework: FastAPI 0.110+ (Python 3.11+)
- ORM: SQLAlchemy 2.0
- Autenticación: Keycloak (OpenID Connect)
- Base de Datos: PostgreSQL 15+
- Validación: Pydantic 2.6+
- Testing: Pytest
Frontend
- Framework: SvelteKit 2.0+ (Svelte 5)
- Lenguaje: TypeScript
- Auth Client: keycloak-js
- Estilos: TailwindCSS 4.1+
- Build: Vite 7+
Infraestructura
- Containerización: Docker / Docker Compose
- Orquestación: Kubernetes (futuro)
- CI/CD: GitHub Actions / GitLab CI
- Monitoreo: Prometheus + Grafana
Arquitectura del Sistema
Patrón Arquitectónico: Modular Layered (estilo NestJS)
┌─────────────────────────────────────────────────────────┐
│ FRONTEND │
│ SvelteKit + Keycloak-js + TailwindCSS │
└────────────────┬────────────────────────────────────────┘
│ HTTP/REST + JWT
┌────────────────▼────────────────────────────────────────┐
│ API GATEWAY (FastAPI) │
│ Middleware: Tenant | License | Logging | CORS │
└────────────────┬────────────────────────────────────────┘
│
┌────────┴────────┐
│ │
┌───────▼──────┐ ┌──────▼────────┐
│ MODULES │ │ CORE LAYER │
│ │ │ │
│ • auth │ │ • config.py │
│ • tenants │ │ • database.py │
│ • licenses │ │ • security.py │
│ • ... │ │ • middleware │
└───────┬──────┘ └───────────────┘
│
┌───────▼──────────────────────────┐
│ DATABASE LAYER (Multi-tenant) │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ Core DB │ │ Tenant 1 DB │ │
│ │ (shared) │ │ (dedicated) │ │
│ └──────────┘ └──────────────┘ │
└──────────────────────────────────┘
Estructura Modular (por módulo)
Cada módulo sigue el patrón:
modules/{module_name}/
├── models.py # ORM Models (SQLAlchemy)
├── dto.py # Data Transfer Objects (Pydantic)
├── service.py # Business Logic Layer
├── routes.py # API Endpoints (FastAPI)
└── __init__.py # Module exports
Responsabilidades por Capa
-
models.py: Representación de entidades en BD
- Define tablas con SQLAlchemy
- Relaciones entre entidades
- Constraints y validaciones a nivel DB
-
dto.py: Contratos de entrada/salida de datos
- DTOs de request (CreateDTO, UpdateDTO)
- DTOs de response (ResponseDTO)
- Validaciones de Pydantic
-
service.py: Lógica de negocio
- Operaciones CRUD
- Validaciones de negocio
- Orquestación de operaciones complejas
-
routes.py: Exposición HTTP
- Definición de endpoints
- Documentación OpenAPI automática
- Manejo de dependencias (auth, db)
Arquitectura de Schemas y Módulos
Estructura de Schemas en Base de Datos
La aplicación utiliza una arquitectura de schemas para organizar lógicamente las tablas según su funcionalidad y alcance:
Schema a24 (Anexo 24)
Contiene todas las tablas relacionadas con el Anexo 24 del SAT (control de inventarios para empresas IMMEX):
- Gestión de inventarios
- Control de entradas y salidas de mercancías
- Reportes de existencias
- Cumplimiento de obligaciones fiscales del Anexo 24
Schema a76 (Anexo 76)
Contiene todas las tablas relacionadas con el Anexo 76 del SAT (comercio exterior):
- Pedimentos aduanales
- Facturas de importación/exportación
- Documentación de comercio exterior
- Cumplimiento normativo de comercio exterior
Schema public (Catálogos Fijos)
Contiene catálogos compartidos y datos de referencia que no cambian frecuentemente:
- Catálogos del SAT (tipos de material, unidades de medida, etc.)
- Códigos de país
- Catálogos de aduanas
- Tipos de documento
- Datos maestros compartidos entre módulos
Convención de Prefijos de Tablas
Para mantener claridad y trazabilidad, las tablas utilizan prefijos que identifican su módulo funcional:
Prefijo inv_ (Inventarios)
Tablas relacionadas con el control de inventarios:
inv_products: Productos en inventarioinv_movements: Movimientos de entrada/salidainv_warehouses: Almacenesinv_balances: Saldos de inventario
Nota histórica: Anteriormente se utilizaba el prefijo s (SCAII - Sistema de aduanas e Inventarios).
Prefijo fa_ (Fixed Assets / Activos Fijos)
Tablas relacionadas con la gestión de activos fijos:
fa_assets: Registro de activos fijosfa_depreciation: Depreciación de activosfa_maintenance: Mantenimiento de activosfa_transfers: Transferencias de activos
Nota histórica: Anteriormente se utilizaba el prefijo q (SCAF - Sistema de Control de Activos Fijos).
Diagrama de Arquitectura de Schemas
┌─────────────────────────────────────────────────────────────┐
│ DATABASE: anexo76_db │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
│ │ Schema: a24 │ │ Schema: a76 │ │Schema: public │ │
│ │ (Anexo 24) │ │ (Anexo 76) │ │ (Catálogos) │ │
│ ├────────────────┤ ├────────────────┤ ├───────────────┤ │
│ │ │ │ │ │ │ │
│ │ inv_products │ │ pedimentos │ │ material_types│ │
│ │ inv_movements │ │ facturas │ │ uom_codes │ │
│ │ inv_warehouses │ │ customs_docs │ │ countries │ │
│ │ inv_balances │ │ export_ops │ │ customs_list │ │
│ │ │ │ │ │ document_types│ │
│ │ fa_assets │ │ │ │ │ │
│ │ fa_depreciation│ │ │ │ │ │
│ │ fa_maintenance │ │ │ │ │ │
│ │ fa_transfers │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────┘ └────────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Ventajas de esta Arquitectura
- Separación Lógica: Cada schema representa un dominio específico del negocio
- Escalabilidad: Facilita la adición de nuevos módulos sin afectar los existentes
- Seguridad: Permite aplicar permisos a nivel de schema
- Mantenibilidad: Código y migraciones organizados por dominio
- Claridad: Los prefijos hacen evidente la funcionalidad de cada tabla
- Migración Gradual: Permite actualizar sistemas legados (SCAII/SCAF) sin interrupciones
Mapeo de Sistemas Legados
| Sistema Legacy | Prefijo Antiguo | Sistema Nuevo | Prefijo Nuevo | Schema |
|---|---|---|---|---|
| SCAII (Inventarios) | s |
Inventarios | inv_ |
a24 |
| SCAF (Activos Fijos) | q |
Fixed Assets | fa_ |
a24 |
| Winsaii (Pedimentos) | w |
- | - | a22 |
| - | g |
Comercio Exterior | - | a76 |
| - | g |
Catálogos SAT | - | public |
Estructura del Proyecto
anexo76/
├── backend/
│ ├── main.py # Aplicación FastAPI principal
│ ├── requirements.txt # Dependencias
│ ├── init_db.py # Script de inicialización
│ ├── Dockerfile
│ │
│ ├── core/ # Capa core (shared)
│ │ ├── config.py # Configuración (Pydantic Settings)
│ │ ├── database.py # Gestión de BD multi-tenant
│ │ ├── security.py # Auth Keycloak + JWT
│ │ ├── middleware.py # Middlewares personalizados
│ │ └── __init__.py
│ │
│ └── api/
│ └── v1/
│ ├── router.py # Router principal v1
│ ├── common/ # Utilidades compartidas
│ │ ├── base_models.py
│ │ ├── crud_routes.py
│ │ ├── dto_mixins.py
│ │ └── tenant_crud_routes.py
│ │
│ └── modules/ # Módulos de negocio por schema
│ ├── a24/ # Módulo Anexo 24 (Inventarios)
│ │ ├── inventarios/
│ │ └── activos_fijos/
│ │
│ ├── a76/ # Módulo Anexo 76 (Comercio Exterior)
│ │ ├── pedimentos/
│ │ └── facturas/
│ │
│ └── public/ # Catálogos compartidos
│ ├── material_types/
│ ├── uom_codes/
│ └── countries/
│
├── frontend/
│ ├── src/
│ │ ├── routes/ # Páginas SvelteKit
│ │ │ ├── +layout.svelte # Layout global con Keycloak
│ │ │ ├── +page.svelte # Dashboard principal
│ │ │ └── callback/ # OAuth callback
│ │ │
│ │ └── lib/
│ │ ├── auth.ts # Servicio de autenticación
│ │ └── api.ts # Cliente API
│ │
│ ├── static/
│ │ └── silent-check-sso.html
│ ├── package.json
│ └── Dockerfile
│
├── docs/
│ ├── KEYCLOAK_SETUP.md # Guía de configuración
│ └── ARCHITECTURE.md # Este documento
│
├── docker-compose.yml # Orquestación completa
├── start.sh # Script de inicio rápido
├── README.md # Documentación principal
└── .gitignore
Flujos Principales
1. Flujo de Autenticación
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Frontend │ │ Keycloak │ │ Backend │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. Clic "Login" │ │
├────────────────────────────>│ │
│ │ │
│ 2. Formulario de login │ │
│<────────────────────────────┤ │
│ │ │
│ 3. Credenciales │ │
├────────────────────────────>│ │
│ │ │
│ 4. Redirigir + auth code │ │
│<────────────────────────────┤ │
│ │ │
│ 5. Intercambiar code x token│ │
├────────────────────────────>│ │
│ │ │
│ 6. JWT (access + refresh) │ │
│<────────────────────────────┤ │
│ │ │
│ 7. Request con Bearer token │ │
├─────────────────────────────┼──────────────────────────>│
│ │ │
│ │ 8. Validar token │
│ │<──────────────────────────┤
│ │ │
│ │ 9. Public key │
│ ├──────────────────────────>│
│ │ │
│ 10. Respuesta con datos │ │
│<─────────────────────────────┼───────────────────────────┤
│ │ │
2. Flujo de Request Multi-tenant
Request con JWT
↓
TenantMiddleware
├─ Extrae tenant_id del token
├─ Valida tenant existe y está activo
└─ Agrega tenant_id a request.state
↓
LicenseValidationMiddleware
├─ Consulta licencia del tenant
├─ Valida estado (active/expired)
├─ Valida fecha de vigencia
└─ Agrega license_info a request.state
↓
Endpoint Handler
├─ Obtiene tenant_id de request.state
├─ Selecciona BD (shared o dedicated)
└─ Procesa request
↓
Response
3. Flujo de Selección de Base de Datos
# Pseudocódigo
tenant_id = request.state.tenant_id
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
if tenant.type == "SHARED":
# Usar BD compartida (core_db)
db_session = CoreSessionLocal()
# Queries incluyen tenant_id en WHERE
elif tenant.type == "DEDICATED":
# Usar BD dedicada del tenant
db_config = json.loads(tenant.db_config)
db_session = get_tenant_db(tenant_id, db_config)
# No necesita filtrar por tenant_id
Seguridad
Autenticación
- Keycloak como Identity Provider
- OpenID Connect (OIDC)
- JWT con RS256 (firma asimétrica)
- Refresh tokens para renovación
Autorización
- RBAC (Role-Based Access Control)
- Roles:
admin,user,auditor,system - Middleware
has_role()para proteger endpoints
Multi-tenancy
- Aislamiento por tenant_id en JWT
- Row-level security en BD compartida
- BD dedicada para mayor aislamiento (enterprise)
Row-Level Security (RLS) en BD compartida
Convención alineada al skill
aduanasoft-dev-standards(sección 10). La capa API sigue siendo responsable del control fino (roles/permisos con Keycloak +PermissionService); RLS añade defensa en profundidad a nivel de BD para que un bug en unWHEREno permita salirse del tenant.
Variables de sesión (SET LOCAL)
| GUC | Origen | Comportamiento RLS |
|---|---|---|
app.tenant_id |
JWT (TenantMiddleware) → request.state.tenant_id |
Obligatoria. Si está vacía, app.current_tenant_id() retorna NULL y las políticas devuelven 0 filas (fail-closed). |
app.company_id |
Header X-Company-Id o cookie active_company_id |
Opcional. Si está vacía, el tenant ve todas sus compañías (útil para selectores de compañía y bootstrap). |
Ambas se fijan con SET LOCAL al inicio de cada transacción —
nunca con SET global, para no contaminar conexiones del pool.
Helpers SQL definidos por la migración d1a2b3c4e5f6_enable_rls_tenant_company:
CREATE FUNCTION app.current_tenant_id() RETURNS INTEGER LANGUAGE sql STABLE AS
$$ SELECT NULLIF(current_setting('app.tenant_id', true), '')::INTEGER $$;
CREATE FUNCTION app.current_company_id() RETURNS INTEGER LANGUAGE sql STABLE AS
$$ SELECT NULLIF(current_setting('app.company_id', true), '')::INTEGER $$;
Tipos de política
- Solo
tenant_id(p.ej.a76.company,core.licenses):tenant_id = app.current_tenant_id(). tenant_id+company_id(TenantScopedMixin, mayoría de tablasa24/a76/core): además exigecompany_id = app.current_company_id()cuando esa GUC está fijada.- Solo
company_id(algunas tablasa76.company_*): valida eltenant_idindirectamente víaEXISTScontraa76.company.
Todas las tablas usan FORCE ROW LEVEL SECURITY para que la política
aplique también al owner. Las únicas tablas core excluidas son
core.tenants y core.user_tenants — necesarias para el bootstrap del
selector de tenant antes de tener contexto fijado.
Propagación del contexto
| Camino | Cómo se fija el contexto |
|---|---|
| HTTP request | TenantMiddleware rellena request.state.tenant_id/company_id; get_core_db / get_async_core_db leen esos valores y los guardan en Session.info. Un listener after_begin ejecuta SET LOCAL por transacción. |
LicenseValidationMiddleware |
Usa scoped_core_db(tenant_id=...) para que la consulta de licencia entre con contexto RLS válido. |
| Tareas Celery | track_and_dispatch inyecta rls_tenant_id / rls_company_id en los headers del task; los signals task_prerun/task_postrun los copian a ContextVars del worker, que el listener after_begin consume como fallback. Tareas críticas (imports/exports de invoices, expediente) abren la sesión con scoped_core_db(tenant_id=..., company_id=...). |
| Tests | Las suites de pytest pueden usar scoped_core_db(...) o emular el flujo con set_config('app.tenant_id', ...) antes del query. Hay un set de tests en backend/tests/integration/test_rls_tenant_company.py que valida aislamiento A vs B usando un rol sin BYPASSRLS. |
Reparto de responsabilidades
| Capa | Decide |
|---|---|
API (FastAPI + Keycloak + PermissionService) |
Roles, permisos por compañía, accesos a recursos concretos (validate_access_to_resource), reglas de negocio. |
| RLS (PostgreSQL) | Límite estructural duro: tenant_id y company_id. No modela roles/permisos para evitar duplicar lógica fina con la API. |
Operación / DevOps
- En producción la API debe conectar con un rol sin
BYPASSRLS(postgressuperusuario lo bypassea por diseño). Eldocker-compose.ymlde desarrollo usapostgresdeliberadamente para no romper migraciones; los tests crean un rolanexo76_rls_testpara ejercitar las políticas. - Los jobs/ETL/migraciones que necesiten ver todos los tenants deben usar
un rol técnico explícito con
BYPASSRLSo fijarapp.tenant_idpor iteración — nunca asumir que la sesión global "ve todo". - La migración
d1a2b3c4e5f6_enable_rls_tenant_companytienedowngrade()completo (drop policies +DISABLE ROW LEVEL SECURITY) para revertir.
Validación de Licencias
- Middleware verifica en cada request:
- ✓ Licencia activa
- ✓ No expirada
- ✓ Límites no excedidos
Base de Datos
Modelo Híbrido Multi-tenant
BD Core (Compartida)
Tablas principales:
tenants: Información de clienteslicenses: Control de licencias por tenantlicense_usage: Métricas de usousers(futuro): Usuarios por tenant
Todas las tablas operacionales incluyen tenant_id para segmentación.
BD Dedicadas (Enterprise)
- Una BD PostgreSQL por tenant
- Configuración almacenada en
tenants.db_config - Migración automática desde BD compartida
Ejemplo de Tabla Multi-tenant
CREATE TABLE inventories (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
product_code VARCHAR(50) NOT NULL,
quantity INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
-- Índice compuesto para queries eficientes
INDEX idx_tenant_product (tenant_id, product_code)
);
Migración y Upgrade
# Tenant en BD compartida → BD dedicada
tenant_service.upgrade_to_dedicated(
tenant_id=123,
db_config={
"host": "dedicated-postgres.example.com",
"port": 5432,
"name": "tenant_123_db",
"user": "tenant_123_user",
"password": "secure_password"
}
)
API Reference
Módulo: Authentication (/v1/auth)
| Endpoint | Método | Descripción | Auth |
|---|---|---|---|
/auth/login |
POST | Login con Keycloak | Público |
/auth/refresh |
POST | Renovar access token | Público |
/auth/me |
GET | Info del usuario actual | Bearer |
/auth/logout |
POST | Cerrar sesión | Bearer |
/auth/health |
GET | Health check | Público |
Módulo: Tenants (/v1/tenants)
| Endpoint | Método | Descripción | Rol Requerido |
|---|---|---|---|
/tenants |
POST | Crear tenant | admin |
/tenants |
GET | Listar tenants | admin |
/tenants/{id} |
GET | Obtener tenant | user |
/tenants/{id} |
PUT | Actualizar tenant | admin |
/tenants/{id} |
DELETE | Eliminar tenant | admin |
/tenants/slug/{slug} |
GET | Obtener por slug | user |
Módulo: Licenses (/v1/licenses)
| Endpoint | Método | Descripción | Rol Requerido |
|---|---|---|---|
/licenses |
POST | Crear licencia | admin |
/licenses/tenant/{id} |
GET | Obtener licencia | user |
/licenses/tenant/{id} |
PUT | Actualizar licencia | admin |
/licenses/validate/{id} |
GET | Validar licencia | user |
/licenses/usage/{id} |
GET | Uso de licencia | user |
/licenses/my-license |
GET | Mi licencia | user |
Planes de Licencia
| Plan | Usuarios | Storage | Operaciones/mes | Features |
|---|---|---|---|---|
| Free | 5 | 10 GB | 1,000 | API básica |
| Basic | 20 | 50 GB | 10,000 | + Reportes |
| Professional | 100 | 200 GB | 50,000 | + Integraciones |
| Enterprise | ∞ | ∞ | ∞ | + Soporte + BD dedicada |
Próximas Implementaciones
Backend
- Módulo de inventarios
- Módulo de pedimentos
- Módulo de facturas
- Webhooks para integraciones
- Reportes avanzados
- Export/Import de datos
Frontend
- Dashboard con gráficas
- Gestión de inventarios UI
- Formularios de pedimentos
- Panel de administración
- Reportes interactivos
DevOps
- CI/CD pipeline
- Tests automatizados
- Monitoreo con Prometheus
- Dashboards de Grafana
- Deploy a Kubernetes
- Backup automatizado
Última actualización: Octubre 2025
Versión del documento: 1.0