Files
plantillas-proyectos/docs/ARCHITECTURE.md

25 KiB

Anexo76 - Resumen de Arquitectura Técnica

📋 Índice

  1. Visión General
  2. Stack Tecnológico
  3. Arquitectura del Sistema
  4. Arquitectura de Schemas y Módulos
  5. Estructura del Proyecto
  6. Flujos Principales
  7. Seguridad
  8. Base de Datos
  9. 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

  1. models.py: Representación de entidades en BD

    • Define tablas con SQLAlchemy
    • Relaciones entre entidades
    • Constraints y validaciones a nivel DB
  2. dto.py: Contratos de entrada/salida de datos

    • DTOs de request (CreateDTO, UpdateDTO)
    • DTOs de response (ResponseDTO)
    • Validaciones de Pydantic
  3. service.py: Lógica de negocio

    • Operaciones CRUD
    • Validaciones de negocio
    • Orquestación de operaciones complejas
  4. 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 inventario
  • inv_movements: Movimientos de entrada/salida
  • inv_warehouses: Almacenes
  • inv_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 fijos
  • fa_depreciation: Depreciación de activos
  • fa_maintenance: Mantenimiento de activos
  • fa_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

  1. Separación Lógica: Cada schema representa un dominio específico del negocio
  2. Escalabilidad: Facilita la adición de nuevos módulos sin afectar los existentes
  3. Seguridad: Permite aplicar permisos a nivel de schema
  4. Mantenibilidad: Código y migraciones organizados por dominio
  5. Claridad: Los prefijos hacen evidente la funcionalidad de cada tabla
  6. 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 un WHERE no 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

  1. Solo tenant_id (p.ej. a76.company, core.licenses): tenant_id = app.current_tenant_id().
  2. tenant_id + company_id (TenantScopedMixin, mayoría de tablas a24/a76/core): además exige company_id = app.current_company_id() cuando esa GUC está fijada.
  3. Solo company_id (algunas tablas a76.company_*): valida el tenant_id indirectamente vía EXISTS contra a76.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 (postgres superusuario lo bypassea por diseño). El docker-compose.yml de desarrollo usa postgres deliberadamente para no romper migraciones; los tests crean un rol anexo76_rls_test para ejercitar las políticas.
  • Los jobs/ETL/migraciones que necesiten ver todos los tenants deben usar un rol técnico explícito con BYPASSRLS o fijar app.tenant_id por iteración — nunca asumir que la sesión global "ve todo".
  • La migración d1a2b3c4e5f6_enable_rls_tenant_company tiene downgrade() 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 clientes
  • licenses: Control de licencias por tenant
  • license_usage: Métricas de uso
  • users (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