feat: plantilla base workspace SaaS
Some checks failed
Build Producción & Push a Harbor / test (push) Failing after 3s
Build Producción & Push a Harbor / build (push) Has been skipped
Aduanasoft/plantillas-proyectos/pipeline/head There was a failure building this commit

This commit is contained in:
2026-07-21 13:59:00 -05:00
commit bdd089954b
470 changed files with 70022 additions and 0 deletions

599
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,599 @@
# Anexo76 - Resumen de Arquitectura Técnica
## 📋 Índice
1. [Visión General](#visión-general)
2. [Stack Tecnológico](#stack-tecnológico)
3. [Arquitectura del Sistema](#arquitectura-del-sistema)
4. [Arquitectura de Schemas y Módulos](#arquitectura-de-schemas-y-módulos)
5. [Estructura del Proyecto](#estructura-del-proyecto)
6. [Flujos Principales](#flujos-principales)
7. [Seguridad](#seguridad)
8. [Base de Datos](#base-de-datos)
9. [API Reference](#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
```python
# 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`:
```sql
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 `ContextVar`s 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
```sql
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
```python
# 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

227
docs/KEYCLOAK_SETUP.md Normal file
View File

@@ -0,0 +1,227 @@
# Guía de Configuración de Keycloak para Anexo76
Esta guía te ayudará a configurar Keycloak para usar con Anexo76.
# Script auto initialize
Te genera toda la configruracion inicial de keycloack que se ve en este documento,
aparte de esto te genera un primer usuario configurado con su tenant y una company
```
scripts/init_first_time.sh
```
## 1. Acceder a Keycloak Admin Console
1. Abrir http://localhost:8080
2. Hacer clic en "Administration Console"
3. Login con: `admin` / `admin`
## 2. Configurar Cliente Backend
### Crear Cliente Backend
1. En el menú izquierdo, ir a **Clients**
2. Clic en **Create client**
3. Configurar:
- **Client ID**: `anexo76-backend`
- **Client Protocol**: `openid-connect`
- Clic en **Next**
4. En la siguiente pantalla:
- **Client authentication**: ON (Confidential)
- **Authorization**: OFF
- **Authentication flow**: Marcar solo "Standard flow" y "Direct access grants"
- Clic en **Next**
5. En "Login settings":
- **Root URL**: `http://localhost:8000`
- **Valid redirect URIs**: `http://localhost:8000/*`
- **Web origins**: `http://localhost:8000`
- Clic en **Save**
### Obtener Client Secret
1. Ir a la pestaña **Credentials**
2. Copiar el **Client secret**
3. Agregar al archivo `backend/.env`:
```
KEYCLOAK_CLIENT_SECRET=tu-client-secret-aqui
```
## 3. Configurar Cliente Frontend
### Crear Cliente Frontend
1. En **Clients**, clic en **Create client**
2. Configurar:
- **Client ID**: `anexo76-frontend`
- **Client Protocol**: `openid-connect`
- Clic en **Next**
3. En la siguiente pantalla:
- **Client authentication**: OFF (Public)
- **Authorization**: OFF
- **Authentication flow**: Marcar "Standard flow"
- Clic en **Next**
4. En "Login settings":
- **Root URL**: `http://localhost:5173`
- **Valid redirect URIs**:
- `http://localhost:5173/*`
- `http://localhost:3000/*`
- **Valid post logout redirect URIs**:
- `http://localhost:5173/*`
- `http://localhost:3000/*`
- **Web origins**:
- `http://localhost:5173`
- `http://localhost:3000`
- Clic en **Save**
## 4. Crear Usuario de Prueba
### Crear Usuario
1. En el menú izquierdo, ir a **Users**
2. Clic en **Add user**
3. Configurar:
- **Username**: `demo`
- **Email**: `demo@empresa-demo.com`
- **First name**: `Usuario`
- **Last name**: `Demo`
- **Email verified**: ON
- Clic en **Create**
### Establecer Contraseña
1. Ir a la pestaña **Credentials**
2. Clic en **Set password**
3. Configurar:
- **Password**: `demo123`
- **Password confirmation**: `demo123`
- **Temporary**: OFF (para no tener que cambiar la contraseña)
4. Clic en **Save**
### Agregar Atributo tenant_id
1. En el mismo usuario, ir a la pestaña **Attributes**
2. Clic en **Add an attribute**
3. Configurar:
- **Key**: `tenant_id`
- **Value**: `1`
4. Clic en **Save**
### Asignar Roles
1. Ir a la pestaña **Role mappings**
2. En "Available roles", buscar y asignar:
- `admin` (si existe)
- `user` (si existe)
3. Si no existen estos roles, crearlos primero:
- Ir a **Realm roles** en el menú izquierdo
- Crear roles: `admin`, `user`, `auditor`, `system`
- Regresar al usuario y asignar roles
## 5. Configurar Mapper para tenant_id (Opcional pero recomendado)
Para que el `tenant_id` se incluya automáticamente en el token:
1. Ir a **Clients** → `anexo76-backend`
2. Ir a la pestaña **Client scopes**
3. Clic en `anexo76-backend-dedicated`
4. Ir a la pestaña **Mappers**
5. Clic en **Add mapper** → **By configuration** → **User Attribute**
6. Configurar:
- **Name**: `tenant-id-mapper`
- **User Attribute**: `tenant_id`
- **Token Claim Name**: `tenant_id`
- **Claim JSON Type**: `String`
- **Add to ID token**: ON
- **Add to access token**: ON
- **Add to userinfo**: ON
7. Clic en **Save**
Repetir para el cliente `anexo76-frontend` si es necesario.
## 6. Verificar Configuración
### Probar desde el Frontend
1. Abrir http://localhost:5173
2. Hacer clic en "Iniciar Sesión"
3. Ingresar credenciales:
- Usuario: `demo`
- Contraseña: `demo123`
4. Deberías ver el dashboard con información del usuario y licencia
### Probar desde el API
```bash
# Obtener token
curl -X POST http://localhost:8080/realms/master/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=anexo76-backend" \
-d "client_secret=TU_CLIENT_SECRET" \
-d "username=demo" \
-d "password=demo123" \
-d "grant_type=password"
# Usar el token para llamar al API
curl -X GET http://localhost:8000/v1/auth/me \
-H "Authorization: Bearer TU_ACCESS_TOKEN"
```
## 7. Configuración Adicional (Opcional)
### Personalizar Tema de Login
1. Ir a **Realm settings** → **Themes**
2. Seleccionar tema de login deseado
3. Guardar cambios
### Configurar Timeout de Sesión
1. Ir a **Realm settings** → **Sessions**
2. Ajustar:
- **SSO Session Idle**: Tiempo de inactividad antes de expirar (ej: 30 minutos)
- **SSO Session Max**: Tiempo máximo de sesión (ej: 10 horas)
3. Guardar cambios
### Habilitar Registro de Usuarios (Opcional)
1. Ir a **Realm settings** → **Login**
2. Activar **User registration**
3. Guardar cambios
## Troubleshooting
### Error: "Invalid redirect URI"
- Verificar que las URIs en el cliente coincidan exactamente
- Incluir el protocolo (http:// o https://)
- Incluir el puerto si es necesario
### Error: "Client not found"
- Verificar que el Client ID sea exacto
- Verificar que el realm sea correcto
### Token no incluye tenant_id
- Verificar que el usuario tenga el atributo configurado
- Verificar que el mapper esté configurado correctamente
- Probar obteniendo un nuevo token
### Usuario no puede hacer login
- Verificar que el usuario esté habilitado (User enabled: ON)
- Verificar que el email esté verificado (Email verified: ON)
- Verificar que la contraseña no sea temporal
## Próximos Pasos
1. Para producción, cambiar el realm de `master` a uno dedicado
2. Configurar HTTPS/TLS en Keycloak
3. Configurar backup de la base de datos de Keycloak
4. Implementar políticas de contraseña más estrictas
5. Configurar MFA (Multi-Factor Authentication)
---
**¡Listo!** Tu configuración de Keycloak está completa para desarrollo.

214
docs/MICROSOFT_SSO_SETUP.md Normal file
View File

@@ -0,0 +1,214 @@
# Configuración de Login con Microsoft (Azure AD)
Esta guía te ayudará a configurar el login con Microsoft junto con el login tradicional.
## Parte 1: Configurar Aplicación en Azure AD
### 1.1 Crear App Registration en Azure Portal
1. Ve a [Azure Portal](https://portal.azure.com)
2. Busca "Azure Active Directory" o "Microsoft Entra ID"
3. En el menú lateral, selecciona **App registrations**
4. Clic en **New registration**
5. Configura:
- **Name**: `Anexo76`
- **Supported account types**:
- "Accounts in any organizational directory (Any Azure AD directory - Multitenant)"
- O "Accounts in any organizational directory and personal Microsoft accounts" si quieres permitir cuentas @outlook.com, @hotmail.com
- **Redirect URI**:
- Platform: `Web`
- URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
- Clic en **Register**
### 1.2 Obtener Client ID y crear Client Secret
1. En la página de tu aplicación, copia el **Application (client) ID**
2. Ve a **Certificates & secrets** en el menú lateral
3. Clic en **New client secret**
4. Descripción: `keycloak-integration`
5. Expires: Selecciona el tiempo que prefieras (ej: 24 months)
6. Clic en **Add**
7. **IMPORTANTE**: Copia el **Value** del secret inmediatamente (solo se muestra una vez)
### 1.3 Configurar API Permissions (Opcional pero recomendado)
1. Ve a **API permissions**
2. Deberías ver `Microsoft Graph` > `User.Read` (Delegated) - esto es suficiente
3. Si quieres más información del usuario, agrega:
- `email`
- `profile`
- `openid`
## Parte 2: Configurar Identity Provider en Keycloak
### 2.1 Agregar Microsoft como Identity Provider
1. Abre Keycloak Admin Console: http://localhost:8080
2. Login como admin
3. Asegúrate de estar en el realm correcto (probablemente `master`)
4. En el menú lateral, ve a **Identity providers**
5. En el dropdown "Add provider", selecciona **Microsoft**
6. Configura:
- **Alias**: `microsoft` (o cualquier nombre que prefieras)
- **Display name**: `Microsoft` (esto es lo que verá el usuario)
- **Enabled**: ON
- **Store tokens**: ON (opcional, para poder usar tokens de Microsoft después)
- **Stored tokens readable**: OFF
- **Trust email**: ON
- **First login flow**: `first broker login`
- **Client ID**: Pega el Application (client) ID de Azure
- **Client Secret**: Pega el client secret que copiaste
- Clic en **Save**
### 2.2 Configurar Mappers (Mapeo de atributos)
Después de guardar, configura los mappers para traer información del usuario de Microsoft:
1. En la misma página del Identity Provider, ve a la pestaña **Mappers**
2. Clic en **Add mapper**
**Mapper 1: Email**
- Name: `email`
- Sync mode override: `inherit`
- Mapper type: `Attribute Importer`
- Social profile JSON field path: `email`
- User attribute name: `email`
- Clic en **Save**
**Mapper 2: First Name**
- Name: `firstName`
- Mapper type: `Attribute Importer`
- Social profile JSON field path: `given_name`
- User attribute name: `firstName`
- Clic en **Save**
**Mapper 3: Last Name**
- Name: `lastName`
- Mapper type: `Attribute Importer`
- Social profile JSON field path: `family_name`
- User attribute name: `lastName`
- Clic en **Save**
**Mapper 4: Username**
- Name: `username`
- Mapper type: `Username Template Importer`
- Template: `${CLAIM.email}`
- Target: `BROKER_USERNAME`
- Clic en **Save**
### 2.3 Configurar Redirect URI en Azure (si es necesario)
Si usas un realm diferente a `master`, actualiza la Redirect URI en Azure:
- Formato: `http://localhost:8080/realms/{REALM_NAME}/broker/microsoft/endpoint`
- Para producción: `https://tu-dominio.com/realms/{REALM_NAME}/broker/microsoft/endpoint`
## Parte 3: Actualizar Frontend
El frontend necesita detectar y mostrar el botón de Microsoft. Keycloak proporciona esta información automáticamente.
### 3.1 Obtener Identity Providers disponibles
Tu frontend puede consultar los Identity Providers disponibles:
**Endpoint de Keycloak:**
```
GET http://localhost:8080/realms/master/broker-login/identity-providers
```
Esto retorna algo como:
```json
[
{
"alias": "microsoft",
"displayName": "Microsoft",
"providerId": "microsoft",
"enabled": true
}
]
```
### 3.2 URL para iniciar flujo de Microsoft
Para iniciar el login con Microsoft, redirige al usuario a:
```
http://localhost:8080/realms/master/broker/microsoft/login?client_id=anexo76-frontend&redirect_uri=http://localhost:5173/auth/callback
```
Parámetros:
- `client_id`: Tu client ID de frontend en Keycloak (`anexo76-frontend`)
- `redirect_uri`: URL a la que Keycloak redirigirá después del login exitoso
- `response_type`: `code` (para authorization code flow)
- `scope`: `openid profile email`
### 3.3 Manejar el Callback
Después del login con Microsoft, Keycloak redirige a tu `redirect_uri` con un `code`:
```
http://localhost:5173/auth/callback?code=abc123...&session_state=xyz...
```
Tu frontend debe:
1. Extraer el `code` del query string
2. Intercambiar el `code` por tokens llamando a tu backend
3. Tu backend llama a Keycloak para obtener los tokens
## Parte 4: Testing
### 4.1 Verificar que Microsoft aparece en la página de login
Ve a:
```
http://localhost:8080/realms/master/protocol/openid-connect/auth?client_id=anexo76-frontend&redirect_uri=http://localhost:5173&response_type=code
```
Deberías ver:
- Formulario de login tradicional (usuario/contraseña)
- Botón o link de "Microsoft" para login social
### 4.2 Probar el flujo completo
1. Haz clic en el botón de Microsoft
2. Serás redirigido a Microsoft login
3. Ingresa credenciales de Microsoft
4. Microsoft redirige a Keycloak
5. Keycloak crea/actualiza el usuario y redirige a tu app
6. Tu app obtiene el token y autentica al usuario
## Notas Importantes
### Multi-tenant con Microsoft
Si tu app es multi-tenant y quieres que cada tenant use su propio Azure AD:
1. Crea múltiples Identity Providers en Keycloak (uno por tenant)
2. Usa aliases diferentes: `microsoft-tenant1`, `microsoft-tenant2`
3. En el frontend, muestra el botón correcto según el tenant
### Asignación automática de tenant
Cuando un usuario se loguea por primera vez con Microsoft, puedes:
1. Usar un mapper para asignar atributos basados en el dominio del email
2. Configurar "Default Tenant" en tu backend si el email es de un dominio conocido
3. Solicitar al usuario que seleccione su tenant en el primer login
### Producción
Para producción, recuerda:
1. Actualizar las Redirect URIs en Azure con tu dominio real
2. Usar HTTPS
3. Configurar correctamente los Web Origins en Keycloak
4. Usar variables de entorno para las configuraciones
## Troubleshooting
### Error: redirect_uri_mismatch
- Verifica que la URI en Azure coincida exactamente con la de Keycloak
- Formato: `https://tu-dominio.com/realms/{realm}/broker/{alias}/endpoint`
### Usuario se crea pero no tiene tenant_id
- Configura un mapper en Keycloak para asignar tenant_id automáticamente
- O maneja esto en tu backend en el primer login
### No aparece el botón de Microsoft
- Verifica que el Identity Provider esté habilitado en Keycloak
- Revisa que el Display Name esté configurado

View File

@@ -0,0 +1,218 @@
# ✅ Verificar Configuración de Microsoft en Keycloak
## Paso 1: Verificar si Microsoft está configurado
Abre tu navegador y ve a:
```
http://localhost:8080/admin/master/console/#/master/identity-providers
```
Login: `admin` / `admin`
### ¿Qué deberías ver?
Si Microsoft está configurado, verás en la lista de Identity Providers:
-**microsoft** (o el alias que hayas usado)
- Con estado: **Enabled**
### Si NO ves "microsoft" en la lista:
**¡Necesitas configurarlo!** Sigue estos pasos:
---
## Paso 2: Configurar Microsoft en Keycloak (SI NO ESTÁ CONFIGURADO)
### 2.1 Crear App en Azure AD PRIMERO
Antes de configurar Keycloak, necesitas una aplicación en Azure:
1. Ve a [Azure Portal](https://portal.azure.com)
2. Busca **Azure Active Directory** o **Microsoft Entra ID**
3. **App registrations****New registration**
4. Configura:
- **Name**: `Anexo76`
- **Supported account types**: `Accounts in any organizational directory (Any Azure AD - Multitenant)`
- **Redirect URI**:
- Platform: `Web`
- URI: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
- Click **Register**
5. **Copia el Application (client) ID** - lo necesitarás
6. Ve a **Certificates & secrets****New client secret**
- Descripción: `keycloak`
- Expira: 24 meses
- Click **Add**
- **¡COPIA EL SECRET VALUE AHORA!** (solo se muestra una vez)
### 2.2 Agregar Microsoft a Keycloak
1. En Keycloak Admin Console: http://localhost:8080
2. Login: `admin` / `admin`
3. Menú lateral: **Identity providers**
4. Dropdown: **Add provider** → Selecciona **Microsoft**
5. Configura:
```
Alias: microsoft
Display name: Microsoft
Enabled: ON ✅
Store tokens: ON ✅
Trust email: ON ✅
First login flow: first broker login
Client ID: [PEGA TU APPLICATION ID DE AZURE]
Client Secret: [PEGA TU SECRET DE AZURE]
```
6. Click **Save**
7. Ve a la pestaña **Mappers** y agrega estos 4 mappers:
**Mapper 1: email**
```
Name: email
Mapper type: Attribute Importer
Social profile JSON field path: email
User attribute name: email
```
**Mapper 2: firstName**
```
Name: firstName
Mapper type: Attribute Importer
Social profile JSON field path: given_name
User attribute name: firstName
```
**Mapper 3: lastName**
```
Name: lastName
Mapper type: Attribute Importer
Social profile JSON field path: family_name
User attribute name: lastName
```
**Mapper 4: username**
```
Name: username
Mapper type: Username Template Importer
Template: ${CLAIM.email}
Target: BROKER_USERNAME
```
---
## Paso 4: Verificar en tu App
1. Asegúrate de que el frontend esté corriendo: `http://localhost:5173`
2. Ve a la página de login: `http://localhost:5173/login`
3. Ingresa un tenant (ej: `aduanasoft`)
4. Click en el botón **"Iniciar sesión con Microsoft"**
### ¿Qué debería pasar?
**Correcto:**
- Te redirige a Microsoft login
- Ves la página de Microsoft pidiendo tu email/contraseña
- Después de autenticarte, vuelves a tu app
**Incorrecto (lo que te está pasando ahora):**
- Te lleva a la página de login de Keycloak
- Ves el usuario "admin" ya logueado
---
## Troubleshooting Común
### Error: "Identity provider not found"
- El alias en Keycloak debe ser exactamente `microsoft` (minúsculas)
- O cambia el código: `loginWithProvider('TU_ALIAS_EXACTO')`
### Error: redirect_uri_mismatch en Azure
- La URI en Azure debe ser EXACTAMENTE:
```
http://localhost:8080/realms/master/broker/microsoft/endpoint
```
- Nota el `/endpoint` al final
### Error: Unexpected error when authenticating with identity provider
```bash
docker cp azure.crt anexo76-keycloak:/
docker exec -it -u root anexo76-keycloak /bin/bash
keytool -importcert -trustcacerts -file /azure.crt \
-keystore /etc/java/java-21-openjdk/java-21-openjdk-21.0.8.0.9-1.el9.x86_64/lib/security/cacerts \
-alias azure-root -storepass changeit -noprompt
```
### Me redirige pero muestra error en Microsoft
- Verifica que el Client ID y Secret en Keycloak sean correctos
- Verifica que la app en Azure esté habilitada
### Funciona pero el usuario no tiene tenant_id
- Esto es normal en el primer login
- Puedes configurar un mapper adicional o manejarlo en tu backend
---
## Comando Rápido de Verificación
Ejecuta esto en una terminal:
```bash
# Verificar si el endpoint del broker existe
curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/realms/master/broker/microsoft/login?client_id=test&redirect_uri=http://localhost"
```
**Resultados:**
- `302` = ✅ Microsoft está configurado (redirige a Microsoft)
- `404` = ❌ Microsoft NO está configurado en Keycloak
- `500` = ⚠️ Hay un error de configuración
---
## Resumen Rápido
**Para que funcione necesitas:**
1. ✅ App Registration en Azure AD con Client ID y Secret
2. ✅ Identity Provider "microsoft" configurado en Keycloak
3. ✅ Redirect URI en Azure: `http://localhost:8080/realms/master/broker/microsoft/endpoint`
4. ✅ Variables de entorno en frontend (.env):
```
VITE_KEYCLOAK_URL=http://localhost:8080
VITE_KEYCLOAK_REALM=master
VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend
```
**El flujo correcto es:**
```
Tu App → Keycloak Broker → Microsoft Login → Keycloak → Tu App
```
**Lo que está pasando ahora:**
```
Tu App → Keycloak Login (porque no encuentra el provider)
```
---
¿Necesitas ayuda con la configuración? Primero verifica en Keycloak Admin Console si existe el Identity Provider "microsoft".

70
docs/a76.json Normal file
View File

@@ -0,0 +1,70 @@
{
"context": {
"project_name": "Anexo76",
"description": "Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT.",
"business_goal": "Ofrecer una plataforma multi-tenant para maquilas, IMMEX y agentes aduanales que permita manejar inventarios, pedimentos y facturas de importación/exportación con control de licencias y cumplimiento normativo."
},
"architecture": {
"frontend": {
"framework": "SvelteKit",
"auth_integration": "keycloak-js",
"ui_goal": "Dashboard moderno, responsivo y rápido para usuarios empresariales."
},
"backend": {
"framework": "FastAPI",
"auth": "Keycloak (OpenID Connect)",
"db_model": "Hybrid multi-tenant",
"shared_db": "Base de datos central para clientes pequeños y medianos",
"dedicated_db": "Bases de datos independientes para clientes grandes o con alta operación",
"features": [
"Conexión dinámica a BD según tenant",
"Middleware para validar licencias y tenants",
"APIs RESTful versionadas (v1, v2...)",
"Separación de capas: models (ORM), dto (Pydantic), service y routes"
],
"module_structure": {
"pattern": "backend/v1/modules/{module_name}/",
"files": {
"models.py": "Definición ORM con SQLAlchemy",
"dto.py": "Definición de Pydantic DTOs para entrada/salida de datos (reemplaza schemas.py)",
"service.py": "Lógica de negocio y validaciones específicas del módulo",
"routes.py": "Endpoints FastAPI que usan los DTOs y servicios"
},
"naming_convention": {
"models": "Representan entidades persistentes (Base de datos)",
"dto": "Data Transfer Objects para transporte entre capas y API",
"service": "Capa de negocio (domain logic)",
"routes": "Exposición HTTP / API layer"
},
"reasoning": "Se utiliza dto.py en lugar de schemas.py para reflejar un enfoque DDD y estilo arquitectónico similar a NestJS, manteniendo compatibilidad total con FastAPI y Pydantic."
}
},
"auth_system": {
"provider": "Keycloak",
"multi_tenant_model": "Un Realm por cliente (tenant)",
"roles": ["admin", "user", "auditor", "system"],
"license_validation": "Middleware que verifica licencia y plan activo antes de procesar cada request"
}
},
"license_management": {
"strategy": "Control centralizado en core_db",
"table_structure": {
"tenant_id": "int",
"plan": "string",
"max_users": "int",
"expires_at": "datetime",
"status": "active|expired|pending"
},
"upgrade_flow": "El cliente puede escalar de BD compartida a BD dedicada manteniendo mismo tenant_id y realm."
},
"dev_ops": {
"containerization": "Docker / Docker Compose",
"orchestration": "Kubernetes (futuro)",
"monitoring": ["Prometheus", "Grafana"],
"ci_cd": "GitHub Actions o GitLab CI"
},
"prompt_usage": {
"instruction": "Cuando uses este JSON, pide a la IA que genere o revise la arquitectura, código base o estrategia de despliegue respetando el modelo híbrido multi-tenant con Keycloak y FastAPI.",
"example_request": "Diseña un flujo de autenticación multi-tenant con Keycloak y FastAPI que detecte automáticamente el tenant y seleccione la base de datos correcta. Usa dto.py en lugar de schemas.py para mantener una arquitectura estilo DDD."
}
}

View File

@@ -0,0 +1,141 @@
# Analisis de flujo de migracion CSV de transporte (sin validaciones)
## Objetivo
Definir como migrar el flujo de importacion CSV de facturas hacia columnas nuevas de transporte, priorizando continuidad operativa y consistencia de IDs en `invoice_logistics`, sin depender de reglas de validacion funcional.
## Estado actual de la rama
- El pipeline actual ya separa `scan` y `commit` en `tasks.py`.
- El mapeo de plantillas (`template_config.py`) aun esta centrado en `NUMERO TRANSPORTE`.
- En `commit`, la persistencia de logistica usa principalmente:
- `carrier_id` / `carrier_int_id` desde `CLAVE TRANSPORTISTA`.
- `transport_type` desde `TIPO TRANSPORTE`.
- `transport_num` desde `NUMERO TRANSPORTE`.
- No existe aun una ruta establecida para columnas nuevas `CLAVE TRANSPORTE` y `NUMERO CAJA`.
## Intencion funcional identificada en los commits analizados
Sin copiar su logica de validacion, la idea de flujo que se quiso introducir es:
1. Separar los datos de transporte en dos entradas semanticas:
- clave de vehiculo (`CLAVE TRANSPORTE`)
- numero de caja/remolque (`NUMERO CAJA`)
2. Mantener compatibilidad con archivos legacy:
- usar `NUMERO TRANSPORTE` como fallback cuando no existan columnas nuevas.
3. Resolver IDs internos en commit contra catalogos:
- `vehicle_key` -> `vehicle_id`
- `trailer_number` -> `trailer_id`
4. Persistir tanto la clave legible (string) como su ID interno (int) en `invoice_logistics`.
5. Mantener paridad de flujo entre scan y commit en cuanto a normalizacion y resolucion de campos, pero sin convertir scan en un bloqueo por validaciones de negocio.
## Flujo propuesto (sin validaciones de negocio)
### 1) Scan (preprocesamiento y trazabilidad)
Objetivo: preparar datos y metadatos, no rechazar por reglas funcionales.
- Leer CSV con `row_from_template`.
- Normalizar celdas relevantes de transporte:
- trim
- reemplazo de NBSP por espacio
- `None`/vacio a cadena vacia
- Construir una estructura por renglon con campos de transporte efectivos:
- `effective_transport_type`
- `effective_vehicle_key`
- `effective_trailer_number`
- `effective_legacy_transport_num` (solo trazabilidad)
- Guardar errores tecnicos (parseo CSV, formato bruto no interpretable), pero no bloquear por reglas de catalogo/negocio.
### 2) Commit (resolucion y persistencia de IDs)
Objetivo: persistir de forma consistente los IDs nuevos de migracion.
- Determinar campos efectivos por precedencia (ver tabla siguiente).
- Resolver `carrier_int_id` con `CLAVE TRANSPORTISTA` (flujo ya existente).
- Resolver `transport_int_id` cuando exista `effective_vehicle_key`:
- lookup en `vehicle.vehicle_key`
- Resolver `trailer_int_id` cuando exista `effective_trailer_number`:
- lookup en `trailer.trailer_number`
- Persistir en `InvoiceLogistics`:
- string keys: `carrier_id`, `transport_id`, `trailer_num`, `transport_num`
- int refs: `carrier_int_id`, `transport_int_id`, `trailer_int_id`
Importante: para migracion, si hay string pero no hay match de ID, **no romper flujo**; persistir string y dejar int en `NULL` (el FK ya permite `SET NULL`).
## Matriz de mapeo CSV -> `invoice_logistics`
| Entrada CSV | Rol | Campo destino (string) | Campo destino (int) | Catalogo/lookup |
|---|---|---|---|---|
| `CLAVE TRANSPORTISTA` | Transportista | `carrier_id` | `carrier_int_id` | `transporter.transporter_key -> transporter_id` |
| `CLAVE TRANSPORTE` | Vehiculo | `transport_id` | `transport_int_id` | `vehicle.vehicle_key -> vehicle_id` |
| `NUMERO CAJA` | Caja/Remolque | `trailer_num` | `trailer_int_id` | `trailer.trailer_number -> trailer_id` |
| `NUMERO TRANSPORTE` (legacy) | Fallback | `transport_num` (siempre trazable) y apoyo para resolver vehicle/trailer segun precedencia | opcional | depende de reglas de precedencia |
| `TIPO TRANSPORTE` | Tipo logistica | `transport_type` | n/a | enum interno |
## Reglas de precedencia de datos (nuevas vs legacy)
Definicion para no generar ambiguedad:
1. Si viene `CLAVE TRANSPORTE`, usarla para `transport_id`.
2. Si viene `NUMERO CAJA`, usarla para `trailer_num`.
3. Si faltan columnas nuevas y viene `NUMERO TRANSPORTE`:
- usarlo como `transport_num` (trazabilidad legacy)
- y usarlo como fallback de resolucion para `transport_id`/`trailer_num` solo cuando el campo nuevo correspondiente este vacio.
4. Nunca sobreescribir un dato nuevo con legacy si el nuevo viene poblado.
Sugerencia para parciales (`actualizar=true`):
- aplicar merge campo a campo: solo actualizar datos de transporte que lleguen informados en CSV; conservar los demas en la fila existente.
## Relacion con la migracion de IDs
La migracion `ca7d3c4e8b2a` ya formaliza:
- `carrier_int_id` -> FK a `transporter.transporter_id`
- `transport_int_id` -> FK a `vehicle.vehicle_id`
- `trailer_int_id` -> FK a `trailer.trailer_id`
Por lo tanto, el flujo CSV debe priorizar:
- resolver claves string de catalogo de forma determinista
- poblar IDs internos cuando haya match
- mantener string keys para trazabilidad y backfill futuro
## Impacto de implementacion por archivo (sin validaciones)
### `backend/api/v1/modules/a76/layouts_csv/facturas/template_config.py`
- Agregar columnas canonicas nuevas en encabezados:
- `CLAVE TRANSPORTE`
- `NUMERO CAJA`
- Mantener `NUMERO TRANSPORTE` como compatibilidad legacy (no eliminar de inmediato).
- Ajustar aliases para tolerar variantes de cabecera.
### `backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py`
- Incorporar helper de resolucion de campos efectivos de transporte:
- prioridad nuevas columnas
- fallback legacy
- En `scan`, registrar estructura normalizada (sin rechazo por reglas de catalogo).
- En `commit`, poblar `InvoiceLogistics` con:
- `transport_id`/`transport_int_id`
- `trailer_num`/`trailer_int_id`
- `transport_num` como legado/trazabilidad
- Mantener comportamiento de no falla por ausencia de match de IDs.
### Nuevo helper recomendado: `backend/api/v1/modules/a76/layouts_csv/facturas/validators/transport_catalog.py`
- Aunque no se usen validaciones de negocio, centralizar funciones de:
- normalizacion de celdas
- resolucion de keys efectivas
- lookups de IDs de vehiculo/remolque
- Evita duplicar logica entre scan y commit.
## Criterios de aceptacion de esta migracion de flujo
- Queda definida una sola fuente de verdad para precedencia de columnas nuevas/legacy.
- `commit` persiste consistentemente claves string e IDs int de logistica.
- El flujo funciona aun cuando no haya match de catalogo (sin bloqueo por validacion).
- No hay ambiguedad entre:
- `transport_id` vs `transport_num`
- `transport_int_id` vs `trailer_int_id`

View File

@@ -0,0 +1,24 @@
# Alt+Numero Keyboard Navigation Matrix
This matrix documents the active `Alt+DigitN` shortcuts used for fast tab/view navigation in dashboard flows.
## Active contexts
| Context | Shortcut Source | Alt+Digit targets | Focus strategy |
| --- | --- | --- | --- |
| `Edit Broker Tabs` | `frontend/src/lib/config/shortcuts/dashboard/customs_brokers/edit.ts` | `general`, `contact`, `address`, `vu`, `doda`, `anam` | `first-input` |
| `Edit Client Provider` | `frontend/src/lib/config/shortcuts/dashboard/clients_and_providers/edit.ts` | `general`, `address`, `programs`, `config` | `first-input` |
| `Formulario Empresa` | `frontend/src/lib/config/shortcuts/dashboard/general_catalogs/company_information/edit.ts` | `general`, `programa`, `responsable`, `certificaciones`, `direcciones`, `config`, `certificados` | `first-input` |
| `Formulario DODA` | `frontend/src/lib/config/shortcuts/dashboard/general_catalogs/doda/edit.ts` | `general`, `transport`, `sat`, `other` | `first-input` |
| `Invoice Edit` | `frontend/src/lib/config/shortcuts/dashboard/invoices/edit.ts` | `general`, `observations`, `items`, `others`, `continuation` | `first-input` |
| `Invoice Item Form (Inventory)` | `frontend/src/lib/config/shortcuts/dashboard/invoices/item/inventory.ts` | `tab1..tab4` mapped to `general`, `clasificacion`, `cantidades`, `otros` | `first-input` |
| `Invoice Item Form (Fixed Asset)` | `frontend/src/lib/config/shortcuts/dashboard/invoices/item/fixed_asset.ts` | `tab1..tab5` mapped to `generales`, `continuacion`, `series`, `etiquetado`, `identificadores` | `first-input` |
| `Part Form` | `frontend/src/lib/config/shortcuts/dashboard/goods/edit.ts` | `general`, `cont1`, `cont2` | `first-input` |
| `Customs Brokers List` | `frontend/src/lib/config/shortcuts/dashboard/customs_brokers/list.ts` | `brokers`, `customs` | `trigger` |
| `Clients Providers List` | `frontend/src/lib/config/shortcuts/dashboard/clients_and_providers/list.ts` | `all`, `clients`, `providers` | `trigger` |
## Notes
- `KeyboardManager` now has explicit focus policy entries for all contexts listed above.
- `fixed_asset` shortcuts remain in place and are now connected in `item-sheet-fa`.
- No shortcut set was removed in this pass; changes are additive/corrective for keyboard-only navigation.