From 9e3274b03a2e1b7325fc02fdf89a0526b44829ef Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 8 May 2026 13:51:24 -0500 Subject: [PATCH 1/7] Update environment variables in docker-compose and refactor RLS context handling in database and middleware. Added error handling for Hub license verification. --- .../api/v1/modules/a76/audit_log/events.py | 15 +- .../modules/core/tasks_tracking/dispatch.py | 10 +- backend/core/database.py | 21 +- backend/core/middleware.py | 218 +++++++++--------- docker-compose.prod.yml | 2 + 5 files changed, 141 insertions(+), 125 deletions(-) diff --git a/backend/api/v1/modules/a76/audit_log/events.py b/backend/api/v1/modules/a76/audit_log/events.py index 3a790310..28f55422 100644 --- a/backend/api/v1/modules/a76/audit_log/events.py +++ b/backend/api/v1/modules/a76/audit_log/events.py @@ -9,7 +9,12 @@ from sqlalchemy.orm import Session from api.v1.modules.a76.general_catalogs.company.models import Company from api.v1.modules.a76.invoices.models import InvoiceHeader -from core.database import rls_company_var, rls_tenant_var +from core.database import ( + RLS_COMPANY_KEY, + RLS_TENANT_KEY, + rls_company_var, + rls_tenant_var, +) from .services.service import AuditService from .utils.serialization import serialize_for_json @@ -56,13 +61,17 @@ def _resolve_audit_company_tenant(session: Session, target) -> tuple: if company_id is not None: resolution_source = "company_self_id" if company_id is None: - company_id = rls_company_var.get() + company_id = session.info.get(RLS_COMPANY_KEY) + if company_id is None: + company_id = rls_company_var.get() if company_id is not None: resolution_source = "rls_context" tenant_id = getattr(target, "tenant_id", None) if tenant_id is None: - tenant_id = rls_tenant_var.get() + tenant_id = session.info.get(RLS_TENANT_KEY) + if tenant_id is None: + tenant_id = rls_tenant_var.get() if tenant_id is not None and resolution_source == "target": resolution_source = "rls_context" diff --git a/backend/api/v1/modules/core/tasks_tracking/dispatch.py b/backend/api/v1/modules/core/tasks_tracking/dispatch.py index a0f60d87..ac25b8e7 100644 --- a/backend/api/v1/modules/core/tasks_tracking/dispatch.py +++ b/backend/api/v1/modules/core/tasks_tracking/dispatch.py @@ -29,8 +29,10 @@ def track_and_dispatch( if company_id is not None: headers["rls_company_id"] = str(int(company_id)) - token_t = rls_tenant_var.set(int(tenant_id)) - token_c = rls_company_var.set(int(company_id) if company_id is not None else None) + prev_tenant = rls_tenant_var.get() + prev_company = rls_company_var.get() + rls_tenant_var.set(int(tenant_id)) + rls_company_var.set(int(company_id) if company_id is not None else None) try: celery_task = task.apply_async( args=args or [], @@ -39,8 +41,8 @@ def track_and_dispatch( headers=headers, ) finally: - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) + rls_tenant_var.set(prev_tenant) + rls_company_var.set(prev_company) tracker = TaskTrackerService(db) tracker.register_dispatch( diff --git a/backend/core/database.py b/backend/core/database.py index 95e937d3..ddf0269b 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -137,12 +137,13 @@ def get_core_db(request: Request = None) -> Generator[Session, None, None]: escriben ``db: Session = Depends(get_core_db)`` siguen funcionando sin cambios porque ``Request`` se resuelve en la capa de dependencia. - Replica el mismo ``(tenant_id, company_id)`` en ContextVars para código que - comparte la transacción sin la misma instancia de sesión (p. ej. listeners). + No se escriben las ContextVars de RLS aquí: las dependencias síncronas con + ``yield`` se ejecutan vía ``contextmanager_in_threadpool`` (hilo worker) y + mezclar ``ContextVar.set`` / ``reset`` entre ese hilo y el bucle asyncio + provoca ``ValueError: ... was created in a different Context``. El aislamiento + RLS se aplica con ``session.info`` (véase ``after_begin`` y audit listeners). """ tenant_id, company_id = _extract_rls_context(request) - token_t = rls_tenant_var.set(tenant_id) - token_c = rls_company_var.set(company_id) db = CoreSessionLocal() db.info[RLS_TENANT_KEY] = tenant_id db.info[RLS_COMPANY_KEY] = company_id @@ -150,15 +151,15 @@ def get_core_db(request: Request = None) -> Generator[Session, None, None]: yield db finally: db.close() - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSession, None]: """Dependency async para obtener sesión con contexto RLS.""" tenant_id, company_id = _extract_rls_context(request) - token_t = rls_tenant_var.set(tenant_id) - token_c = rls_company_var.set(company_id) + prev_tenant = rls_tenant_var.get() + prev_company = rls_company_var.get() + rls_tenant_var.set(tenant_id) + rls_company_var.set(company_id) try: async with AsyncCoreSessionLocal() as session: session.info[RLS_TENANT_KEY] = tenant_id @@ -168,8 +169,8 @@ async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSess finally: await session.close() finally: - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) + rls_tenant_var.set(prev_tenant) + rls_company_var.set(prev_company) @contextmanager diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 654feadf..ff51166f 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -160,117 +160,11 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): logger.info("[license] tenant override propagated to Hub: %s", tenant_override) try: - # Validación contra el Hub Central async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get( f"{settings.HUB_URL}api/v1/auth/verify-license", headers=hub_headers ) - - logger.info(f"🔑 verify-license → status={response.status_code} body={response.text[:300]}") - - if response.status_code == 404: - # Endpoint no existe en este Hub — dejar pasar - return await call_next(request) - - if response.status_code == 200: - data = response.json() - - # Escenario 1: sin licencia asignada o licencia inactiva - if not data.get("valid", False): - message = data.get("message", "Sin licencia asignada para este tenant") - detail = data.get("detail") - reason = data.get("reason") - # Si el Hub reporta token inválido/expirado, devolver 401 para que - # el frontend dispare el auto-refresh (solo se activa con 401/403, no 402). - if _is_token_issue_message(message, detail, reason): - logger.warning( - "[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s", - message, - detail, - reason, - ) - return JSONResponse( - status_code=401, - content={ - "error": "TOKEN_EXPIRED", - "message": message, - "status_code": 401, - } - ) - - logger.warning( - "[license] licencia invalida para tenant=%s | message=%s", - data.get("tenant_slug"), - message, - ) - return JSONResponse( - status_code=402, - content={ - "error": "LICENSE_ERROR", - "message": message, - "status_code": 402, - } - ) - - # Escenario 2: licencia vencida (verificación local de expires_at) - expires_at_str = data.get("expires_at") - if expires_at_str: - try: - expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) - if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - if expires_at < datetime.now(timezone.utc): - logger.warning( - "[license] licencia expirada para tenant=%s | expires_at=%s", - data.get("tenant_slug"), - expires_at_str, - ) - return JSONResponse( - status_code=402, - content={ - "error": "LICENSE_EXPIRED", - "message": f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción.", - "status_code": 402, - } - ) - except (ValueError, TypeError): - pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad - - request.state.license_info = data - return await call_next(request) # <--- Único camino al éxito - - elif response.status_code == 401: - logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)") - return JSONResponse( - status_code=401, - content={ - "error": "TOKEN_EXPIRED", - "message": "Token inválido o expirado.", - "status_code": 401, - } - ) - - elif response.status_code == 403: - return JSONResponse( - status_code=403, - content={ - "error": "FORBIDDEN", - "message": "El Tenant no tiene permisos en el Hub central.", - "status_code": 403, - } - ) - else: - logger.error(f"Hub error status: {response.status_code}") - return JSONResponse( - status_code=503, - content={ - "error": "HUB_ERROR", - "message": "Error en el servidor de licencias.", - "status_code": 503, - } - ) - except (httpx.ConnectError, httpx.TimeoutException) as e: logger.critical(f"❌ CRITICAL: Hub unreachable: {str(e)}") return JSONResponse( @@ -282,12 +176,120 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): } ) except Exception as e: - logger.error(f"Unexpected license error: {str(e)}") + logger.exception("Hub verify-license request failed: %s", e) return JSONResponse( status_code=500, - content={"error": "VALIDATION_ERROR", "message": "Error interno de validación.", "status_code": 500} + content={ + "error": "VALIDATION_ERROR", + "message": "Error interno al contactar el servicio de licencias.", + "status_code": 500, + } ) + logger.info(f"🔑 verify-license → status={response.status_code} body={response.text[:300]}") + + if response.status_code == 404: + # Endpoint no existe en este Hub — dejar pasar + return await call_next(request) + + if response.status_code == 200: + data = response.json() + + # Escenario 1: sin licencia asignada o licencia inactiva + if not data.get("valid", False): + message = data.get("message", "Sin licencia asignada para este tenant") + detail = data.get("detail") + reason = data.get("reason") + # Si el Hub reporta token inválido/expirado, devolver 401 para que + # el frontend dispare el auto-refresh (solo se activa con 401/403, no 402). + if _is_token_issue_message(message, detail, reason): + logger.warning( + "[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s", + message, + detail, + reason, + ) + return JSONResponse( + status_code=401, + content={ + "error": "TOKEN_EXPIRED", + "message": message, + "status_code": 401, + } + ) + + logger.warning( + "[license] licencia invalida para tenant=%s | message=%s", + data.get("tenant_slug"), + message, + ) + return JSONResponse( + status_code=402, + content={ + "error": "LICENSE_ERROR", + "message": message, + "status_code": 402, + } + ) + + # Escenario 2: licencia vencida (verificación local de expires_at) + expires_at_str = data.get("expires_at") + if expires_at_str: + try: + expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + if expires_at < datetime.now(timezone.utc): + logger.warning( + "[license] licencia expirada para tenant=%s | expires_at=%s", + data.get("tenant_slug"), + expires_at_str, + ) + return JSONResponse( + status_code=402, + content={ + "error": "LICENSE_EXPIRED", + "message": f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción.", + "status_code": 402, + } + ) + except (ValueError, TypeError): + pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad + + request.state.license_info = data + return await call_next(request) + + if response.status_code == 401: + logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)") + return JSONResponse( + status_code=401, + content={ + "error": "TOKEN_EXPIRED", + "message": "Token inválido o expirado.", + "status_code": 401, + } + ) + + if response.status_code == 403: + return JSONResponse( + status_code=403, + content={ + "error": "FORBIDDEN", + "message": "El Tenant no tiene permisos en el Hub central.", + "status_code": 403, + } + ) + + logger.error(f"Hub error status: {response.status_code}") + return JSONResponse( + status_code=503, + content={ + "error": "HUB_ERROR", + "message": "Error en el servidor de licencias.", + "status_code": 503, + } + ) + class RequestLoggingMiddleware(BaseHTTPMiddleware): """ diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0edc6fd1..45064db3 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -346,6 +346,8 @@ services: - NODE_ENV=${NODE_ENV:-production} - VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api} - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} + - INTERNAL_HUB_URL=https://workspace.aduanasoft.com + - HUB_URL=https://workspace.aduanasoft.com - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-https://anexo76-dev.aduanasoft.com/kcauth/} - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} From 315c94219baedc8ed8af744807297a129d89cb64 Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 8 May 2026 12:54:19 -0600 Subject: [PATCH 2/7] feature/api-sittar-actualizacion --- .../fractions/tariff_fractions/service.py | 22 +- .../fractions/us_tariff_fractions/routes.py | 6 +- .../modules/core/tasks_tracking/dispatch.py | 5 +- .../v1/modules/sitar/common/base_service.py | 25 +- .../v1/modules/sitar/fracciones/service.py | 33 ++- .../modules/sitar/fracciones_usa/service.py | 42 +++- .../api/v1/modules/sitar/openapi/README.md | 10 + .../sitar/openapi/sitar-api-openapi-3.1.json | 1 + .../api/v1/modules/sitar/prosec/schemas.py | 19 +- backend/api/v1/modules/sitar/tlcs/schemas.py | 22 +- backend/core/celery_app.py | 12 +- backend/core/database.py | 23 +- backend/core/middleware.py | 231 ++++++++++-------- frontend/src/lib/api/dashboard/a76/sitar.ts | 57 ++--- .../goods/fractions/SitarFractionTabs.svelte | 6 +- .../dashboard/goods/parts/partForm.svelte | 8 +- 16 files changed, 304 insertions(+), 218 deletions(-) create mode 100644 backend/api/v1/modules/sitar/openapi/README.md create mode 100644 backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index a98f78eb..80c95b2d 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -164,16 +164,13 @@ class TariffFractionService: search_description = term try: - usa_items = await usa_service.search( + usa_items, total = await usa_service.search_with_total( fraccion=search_term, descripcion=search_description, skip=skip, limit=limit, ) items = [TariffFractionMapper.to_domain_usa(item) for item in usa_items] - total = len(items) + skip - if len(items) == limit: - total += 1 return items, total except Exception as e: import traceback @@ -245,30 +242,25 @@ class TariffFractionService: # Note: Sitar search might not return total count. # We fetch page items. Pagination might be tricky if Sitar doesn't return total. # Assuming Sitar returns a list. - sitar_items = await sitar_service.search( + sitar_items, total = await sitar_service.search_with_total( fraccion=sitar_fraccion, nico=sitar_nico, description=sitar_description, nivel=level_filter, # Dynamic level skip=skip, - limit=limit + limit=limit, ) - + # STRICT API USAGE: # We do NOT fallback to local DB on empty list, as user requested strict API consumption. # We also do NOT attempt enrichment as codes mismatch (API uses '010191A' vs Local '01012101'). - + # Map items items = [TariffFractionMapper.to_domain(item) for item in sitar_items] # Legacy browse behavior: keep table in ascending fracción order. items = sorted(items, key=lambda row: ((row.code or ""), (row.nico or ""))) - - # Estimate total (Sitar service doesn't return total currently) - # If we got full limit, assume there are more. - total = len(items) + skip - if len(items) == limit: - total += 1 # Indicate more pages - + + # total comes from SITAR PaginatedFraccionesResponse (matches API-wide count for the query). return items, total except Exception as e: diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index acda6661..b5130e55 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -103,7 +103,7 @@ async def list_us_tariff_fractions( search_description = search try: - sitar_items = await svc.search( + sitar_items, total = await svc.search_with_total( fraccion=search_term, descripcion=search_description, skip=skip, @@ -118,10 +118,6 @@ async def list_us_tariff_fractions( "pages": 0, } - total = len(sitar_items) + skip - if len(sitar_items) == page_size: - total += 1 - items = [ USTariffFractionResponseDTO.model_validate(_sitar_row_to_us_response_payload(row)) for row in sitar_items diff --git a/backend/api/v1/modules/core/tasks_tracking/dispatch.py b/backend/api/v1/modules/core/tasks_tracking/dispatch.py index a0f60d87..99ea371f 100644 --- a/backend/api/v1/modules/core/tasks_tracking/dispatch.py +++ b/backend/api/v1/modules/core/tasks_tracking/dispatch.py @@ -3,7 +3,7 @@ from typing import Any from celery import Task from sqlalchemy.orm import Session -from core.database import rls_company_var, rls_tenant_var +from core.database import reset_rls_context_tokens, rls_company_var, rls_tenant_var from .service import TaskTrackerService @@ -39,8 +39,7 @@ def track_and_dispatch( headers=headers, ) finally: - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) + reset_rls_context_tokens(token_t, token_c) tracker = TaskTrackerService(db) tracker.register_dispatch( diff --git a/backend/api/v1/modules/sitar/common/base_service.py b/backend/api/v1/modules/sitar/common/base_service.py index a8e1f077..05d104ca 100644 --- a/backend/api/v1/modules/sitar/common/base_service.py +++ b/backend/api/v1/modules/sitar/common/base_service.py @@ -6,7 +6,7 @@ All specific resource services inherit from this. """ import os -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List, Tuple from datetime import datetime, timedelta import httpx @@ -17,6 +17,29 @@ class SitarAPIBaseService: _token: Optional[str] = None _token_expires: Optional[datetime] = None + @staticmethod + def _parse_paginated_list_and_total(data: Any) -> Tuple[List[Any], int]: + """ + Paginated list endpoints (fracciones, fracciones-usa) return: + { "data": [...], "total", "page", "limit", "total_pages" }. + Older responses may be a plain JSON array; then total is len(rows) for that page only. + """ + if isinstance(data, list): + return data, len(data) + if isinstance(data, dict) and isinstance(data.get("data"), list): + rows = data["data"] + raw_total = data.get("total") + total = int(raw_total) if raw_total is not None else len(rows) + return rows, total + raise ValueError( + f"Unexpected SITAR list response shape: {type(data).__name__}" + ) + + @staticmethod + def _unwrap_paginated_list(data: Any) -> List[Any]: + rows, _ = SitarAPIBaseService._parse_paginated_list_and_total(data) + return rows + def __init__(self): """Initialize base service with API credentials""" self.base_url = os.getenv("SITAR_API_URL") diff --git a/backend/api/v1/modules/sitar/fracciones/service.py b/backend/api/v1/modules/sitar/fracciones/service.py index ada6de61..cf848f1d 100644 --- a/backend/api/v1/modules/sitar/fracciones/service.py +++ b/backend/api/v1/modules/sitar/fracciones/service.py @@ -1,7 +1,7 @@ """Fracciones Service""" import asyncio -from typing import Optional, List +from typing import Optional, List, Tuple from ..common import SitarAPIBaseService from .schemas import FraccionesResponse @@ -18,7 +18,7 @@ class FraccionesService(SitarAPIBaseService): cls._instance = cls() return cls._instance - async def search( + async def search_with_total( self, fraccion: Optional[str] = None, nico: Optional[str] = None, @@ -26,8 +26,8 @@ class FraccionesService(SitarAPIBaseService): nivel: Optional[int] = None, skip: int = 0, limit: int = 100, - ) -> List[FraccionesResponse]: - """Search Mexican tariff fractions""" + ) -> Tuple[List[FraccionesResponse], int]: + """Search Mexican tariff fractions; total matches SITAR PaginatedFraccionesResponse.total.""" params = {"skip": skip, "limit": min(limit, 1000)} if fraccion: params["fraccion"] = fraccion @@ -38,8 +38,29 @@ class FraccionesService(SitarAPIBaseService): if nivel is not None: params["nivel"] = nivel - data = await self._make_request("GET", "/api/v1/fracciones/", params=params) - return [FraccionesResponse(**item) for item in data] + raw = await self._make_request("GET", "/api/v1/fracciones/", params=params) + rows, total = self._parse_paginated_list_and_total(raw) + return [FraccionesResponse(**item) for item in rows], total + + async def search( + self, + fraccion: Optional[str] = None, + nico: Optional[str] = None, + description: Optional[str] = None, + nivel: Optional[int] = None, + skip: int = 0, + limit: int = 100, + ) -> List[FraccionesResponse]: + """Search Mexican tariff fractions""" + items, _ = await self.search_with_total( + fraccion=fraccion, + nico=nico, + description=description, + nivel=nivel, + skip=skip, + limit=limit, + ) + return items async def get_by_id(self, sysid: int) -> FraccionesResponse: """Get single Fraccion record by SYSID""" diff --git a/backend/api/v1/modules/sitar/fracciones_usa/service.py b/backend/api/v1/modules/sitar/fracciones_usa/service.py index fe4b513c..56608c61 100644 --- a/backend/api/v1/modules/sitar/fracciones_usa/service.py +++ b/backend/api/v1/modules/sitar/fracciones_usa/service.py @@ -1,7 +1,7 @@ """Fracciones USA Service""" import logging -from typing import Optional, List +from typing import Optional, List, Tuple from ..common import SitarAPIBaseService from .schemas import FraccionesUSAResponse @@ -22,6 +22,24 @@ class FraccionesUSAService(SitarAPIBaseService): cls._instance = cls() return cls._instance + async def search_with_total( + self, + fraccion: Optional[str] = None, + descripcion: Optional[str] = None, + skip: int = 0, + limit: int = 100, + ) -> Tuple[List[FraccionesUSAResponse], int]: + """Search USA tariff fractions; total matches SITAR PaginatedFraccionesUSAResponse.total.""" + params = {"skip": skip, "limit": min(limit, 1000)} + if fraccion: + params["fraccion"] = fraccion + if descripcion: + params["descripcion"] = descripcion + + raw = await self._make_request("GET", "api/v1/fracciones-usa/", params=params) + rows, total = self._parse_paginated_list_and_total(raw) + return [FraccionesUSAResponse(**item) for item in rows], total + async def search( self, fraccion: Optional[str] = None, @@ -30,14 +48,13 @@ class FraccionesUSAService(SitarAPIBaseService): limit: int = 100, ) -> List[FraccionesUSAResponse]: """Search USA tariff fractions""" - params = {"skip": skip, "limit": min(limit, 1000)} - if fraccion: - params["fraccion"] = fraccion - if descripcion: - params["descripcion"] = descripcion - - data = await self._make_request("GET", "api/v1/fracciones-usa/", params=params) - return [FraccionesUSAResponse(**item) for item in data] + items, _ = await self.search_with_total( + fraccion=fraccion, + descripcion=descripcion, + skip=skip, + limit=limit, + ) + return items async def get_by_id(self, consecutivo: int) -> FraccionesUSAResponse: """Get single USA Fraccion record by CONSECUTIVO""" @@ -68,10 +85,9 @@ class FraccionesUSAService(SitarAPIBaseService): params["descripcion"] = descripcion try: - data = service._make_request_sync("GET", "api/v1/fracciones-usa/", params=params) - if not isinstance(data, list): - return [] - return [FraccionesUSAResponse(**item) for item in data] + raw = service._make_request_sync("GET", "api/v1/fracciones-usa/", params=params) + rows, _ = service._parse_paginated_list_and_total(raw) + return [FraccionesUSAResponse(**item) for item in rows] except Exception as exc: logger.warning("SITAR fracciones-usa search_sync failed: %s", exc) return [] diff --git a/backend/api/v1/modules/sitar/openapi/README.md b/backend/api/v1/modules/sitar/openapi/README.md new file mode 100644 index 00000000..25a68fac --- /dev/null +++ b/backend/api/v1/modules/sitar/openapi/README.md @@ -0,0 +1,10 @@ +# SITAR API — especificación OpenAPI rescatada + +| Campo | Valor | +|-------|--------| +| Fuente | `GET http://api.sitar.aduanasoft.com:880/fractions/api/v1/openapi.json` | +| UI Swagger | `http://api.sitar.aduanasoft.com:880/fractions/docs` | +| Rescatado | 2026-05-08 | +| Archivo | [sitar-api-openapi-3.1.json](sitar-api-openapi-3.1.json) (OpenAPI 3.1, «SITAR API - DEV») | + +Las rutas documentadas usan prefijo `/api/v1/...`. Las peticiones desde este proyecto anteponen `{SITAR_API_URL}/fractions/` (ver `SitarAPIBaseService`), por ejemplo: `{SITAR_API_URL}/fractions/api/v1/tlcs/`. diff --git a/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json b/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json new file mode 100644 index 00000000..8437d53a --- /dev/null +++ b/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"SITAR API - DEV","description":"FastAPI application with SQL Server integration","version":"1.0.0"},"paths":{"/api/v1/auth/login":{"post":{"tags":["authentication"],"summary":"Login","description":"Login endpoint - Authenticate user and return JWT token\n\nArgs:\n credentials: Login credentials (username and password)\n \nReturns:\n Access token (Bearer token)\n \nRaises:\n HTTPException: If credentials are invalid","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Token"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["authentication"],"summary":"Get Current User Info","description":"Get current authenticated user information\nEndpoint protegido que requiere un token válido\n\nArgs:\n current_user: Username extracted from JWT token\n \nReturns:\n Current user information","operationId":"get_current_user_info_api_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/aladi2/":{"get":{"tags":["aladi2"],"summary":"Search Aladi2","description":"Search Aladi2 records by FRACCION and/or NICO\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/aladi2?fraccion=8471600101`\n- Solo NICO: `/aladi2?nico=01`\n- Ambos: `/aladi2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/aladi2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_aladi2_api_v1_aladi2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Aladi2Response"},"title":"Response Search Aladi2 Api V1 Aladi2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/aladi2/{sysid}":{"get":{"tags":["aladi2"],"summary":"Get Aladi2 By Id","description":"Get a single Aladi2 record by SYSID\n\n- **sysid**: ID del registro","operationId":"get_aladi2_by_id_api_v1_aladi2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Aladi2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cuotas2/":{"get":{"tags":["cuotas2"],"summary":"Search Cuotas2","description":"Search Cuotas2 records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/cuotas2?fraccion=8471600101`\n- Solo NICO: `/cuotas2?nico=01`\n- Ambos: `/cuotas2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/cuotas2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_cuotas2_api_v1_cuotas2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Cuotas2Response"},"title":"Response Search Cuotas2 Api V1 Cuotas2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cuotas2/{sysid}":{"get":{"tags":["cuotas2"],"summary":"Get Cuotas2 By Id","description":"Get a single Cuotas2 record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_cuotas2_by_id_api_v1_cuotas2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Cuotas2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cupos/":{"get":{"tags":["cupos"],"summary":"Search Cupos","description":"Search Cupos records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/cupos?fraccion=8471600101`\n- Solo NICO: `/cupos?nico=01`\n- Ambos: `/cupos?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/cupos?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_cupos_api_v1_cupos__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuposResponse"},"title":"Response Search Cupos Api V1 Cupos Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cupos/{sysid}":{"get":{"tags":["cupos"],"summary":"Get Cupos By Id","description":"Get a single Cupos record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_cupos_by_id_api_v1_cupos__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CuposResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones/":{"get":{"tags":["fracciones"],"summary":"Search Fracciones","description":"Search Fracciones records by FRACCION and/or NICO\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **nivel**: Nivel de la fracción (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fracciones?fraccion=8471600101`\n- Solo NICO: `/fracciones?nico=01`\n- Solo nivel: `/fracciones?nivel=4`\n- Ambos: `/fracciones?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/fracciones?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_api_v1_fracciones__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"nivel","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Nivel de la fracción (opcional)","title":"Nivel"},"description":"Nivel de la fracción (opcional)"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedFraccionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones/{sysid}":{"get":{"tags":["fracciones"],"summary":"Get Fracciones By Id","description":"Get a single Fracciones record by SYSID\n\n- **sysid**: ID del registro","operationId":"get_fracciones_by_id_api_v1_fracciones__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-usa/":{"get":{"tags":["fracciones-usa"],"summary":"Search Fracciones Usa","description":"Search Fracciones USA records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (busca en FRACCION_SIN_PUNTO, FRACCION_CON_PUNTO y FRACCION_MOSTRAR)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nEjemplos:\n- `/fracciones-usa?fraccion=8471600101`\n- `/fracciones-usa?fraccion=8471.60.01.01`\n- `/fracciones-usa?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_usa_api_v1_fracciones_usa__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedFraccionesUSAResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-usa/{consecutivo}":{"get":{"tags":["fracciones-usa"],"summary":"Get Fracciones Usa By Id","description":"Get a single Fracciones USA record by CONSECUTIVO\n\n**Requiere autenticación Bearer token**\n\n- **consecutivo**: ID del registro","operationId":"get_fracciones_usa_by_id_api_v1_fracciones_usa__consecutivo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"consecutivo","in":"path","required":true,"schema":{"type":"integer","title":"Consecutivo"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesUSAResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-anteriores/":{"get":{"tags":["fracciones-anteriores"],"summary":"Search Fracciones Anteriores","description":"Search Fracciones Anteriores records by FRACCIONACTUAL and/or FRACCIONANTERIOR\n\n**Requiere autenticación Bearer token**\n\n- **fraccion_actual**: Fracción actual (opcional)\n- **fraccion_anterior**: Fracción anterior (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción actual: `/fracciones-anteriores?fraccion_actual=8471600101`\n- Solo fracción anterior: `/fracciones-anteriores?fraccion_anterior=8471600101`\n- Ambas: `/fracciones-anteriores?fraccion_actual=8471600101&fraccion_anterior=8471600102`\n- Todos (con paginación): `/fracciones-anteriores?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_anteriores_api_v1_fracciones_anteriores__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion_actual","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción actual a buscar","title":"Fraccion Actual"},"description":"Fracción actual a buscar"},{"name":"fraccion_anterior","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción anterior a buscar","title":"Fraccion Anterior"},"description":"Fracción anterior a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FraccionesAnterioresResponse"},"title":"Response Search Fracciones Anteriores Api V1 Fracciones Anteriores Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-anteriores/{sysid}":{"get":{"tags":["fracciones-anteriores"],"summary":"Get Fracciones Anteriores By Id","description":"Get a single Fracciones Anteriores record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_fracciones_anteriores_by_id_api_v1_fracciones_anteriores__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesAnterioresResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/":{"get":{"tags":["fundamentos-tlc"],"summary":"Search Fundamentos Tlc","description":"Search Fundamentos TLC records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fundamentos-tlc?fraccion=84716001`\n- Solo NICO: `/fundamentos-tlc?nico=01`\n- Ambos: `/fundamentos-tlc?fraccion=84716001&nico=01`\n- Todos (con paginación): `/fundamentos-tlc?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fundamentos_tlc_api_v1_fundamentos_tlc__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundamentosTLCResponse"},"title":"Response Search Fundamentos Tlc Api V1 Fundamentos Tlc Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/tipat":{"get":{"tags":["fundamentos-tlc"],"summary":"Search Fundamentos Tlc Tipat","description":"Search Fundamentos TLC records by FRACCION and/or NICO where FUNDAMENTO2 = 'TIPAT'\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fundamentos-tlc/tipat?fraccion=84716001`\n- Solo NICO: `/fundamentos-tlc/tipat?nico=01`\n- Ambos: `/fundamentos-tlc/tipat?fraccion=84716001&nico=01`\n- Todos con TIPAT (con paginación): `/fundamentos-tlc/tipat?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)\n\nTodos los resultados tendrán FUNDAMENTO2 = 'TIPAT'","operationId":"search_fundamentos_tlc_tipat_api_v1_fundamentos_tlc_tipat_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundamentosTLCResponse"},"title":"Response Search Fundamentos Tlc Tipat Api V1 Fundamentos Tlc Tipat Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/{sysid}":{"get":{"tags":["fundamentos-tlc"],"summary":"Get Fundamentos Tlc By Id","description":"Get a single Fundamentos TLC record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_fundamentos_tlc_by_id_api_v1_fundamentos_tlc__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundamentosTLCResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/informacion-general/":{"get":{"tags":["informacion-general"],"summary":"Search Informacion General","description":"Search Informacion General records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/informacion-general?fraccion=8471600101`\n- Solo NICO: `/informacion-general?nico=01`\n- Ambos: `/informacion-general?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/informacion-general?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_informacion_general_api_v1_informacion_general__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InformacionGeneralResponse"},"title":"Response Search Informacion General Api V1 Informacion General Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/informacion-general/{sysid}":{"get":{"tags":["informacion-general"],"summary":"Get Informacion General By Id","description":"Get a single Informacion General record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_informacion_general_by_id_api_v1_informacion_general__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InformacionGeneralResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/precios-estimados2/":{"get":{"tags":["precios-estimados2"],"summary":"Search Precios Estimados2","description":"Search Precios Estimados records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/precios-estimados2?fraccion=8471600101`\n- Solo NICO: `/precios-estimados2?nico=01`\n- Ambos: `/precios-estimados2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/precios-estimados2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_precios_estimados2_api_v1_precios_estimados2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PreciosEstimados2Response"},"title":"Response Search Precios Estimados2 Api V1 Precios Estimados2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/precios-estimados2/{sysid}":{"get":{"tags":["precios-estimados2"],"summary":"Get Precios Estimados2 By Id","description":"Get a single Precios Estimados record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_precios_estimados2_by_id_api_v1_precios_estimados2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreciosEstimados2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/prosec/":{"get":{"tags":["prosec"],"summary":"Search Prosec","description":"Search Prosec records by FRACCION, NICO, SECTOR and/or ARTICULO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **sector**: Sector (opcional)\n- **articulo**: Artículo (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/prosec?fraccion=8471600101`\n- Solo NICO: `/prosec?nico=01`\n- Solo sector: `/prosec?sector=01`\n- Solo artículo: `/prosec?articulo=001`\n- Combinaciones: `/prosec?fraccion=8471600101§or=01&articulo=001`\n- Todos (con paginación): `/prosec?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_prosec_api_v1_prosec__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"sector","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Sector a buscar","title":"Sector"},"description":"Sector a buscar"},{"name":"articulo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Artículo a buscar","title":"Articulo"},"description":"Artículo a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProsecResponse"},"title":"Response Search Prosec Api V1 Prosec Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/prosec/{sysid}":{"get":{"tags":["prosec"],"summary":"Get Prosec By Id","description":"Get a single Prosec record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_prosec_by_id_api_v1_prosec__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProsecResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rcg2/":{"get":{"tags":["rcg2"],"summary":"Search Rcg2","description":"Search RCG2 records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/rcg2?fraccion=8471600101`\n- Solo NICO: `/rcg2?nico=01`\n- Ambos: `/rcg2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/rcg2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_rcg2_api_v1_rcg2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RCG2Response"},"title":"Response Search Rcg2 Api V1 Rcg2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rcg2/{sysid}":{"get":{"tags":["rcg2"],"summary":"Get Rcg2 By Id","description":"Get a single RCG2 record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_rcg2_by_id_api_v1_rcg2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RCG2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reit/":{"get":{"tags":["reit"],"summary":"Search Reit","description":"Search REIT records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/reit?fraccion=8471600101`\n- Solo NICO: `/reit?nico=01`\n- Ambos: `/reit?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/reit?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_reit_api_v1_reit__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/REITResponse"},"title":"Response Search Reit Api V1 Reit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reit/{sysid}":{"get":{"tags":["reit"],"summary":"Get Reit By Id","description":"Get a single REIT record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_reit_by_id_api_v1_reit__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/REITResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/requisito-previo/":{"get":{"tags":["requisito-previo"],"summary":"Search Requisito Previo","description":"Search RequisitoPrevio records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/requisito-previo?fraccion=84716001`\n- Solo NICO: `/requisito-previo?nico=01`\n- Ambos: `/requisito-previo?fraccion=84716001&nico=01`\n- Todos (con paginación): `/requisito-previo?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_requisito_previo_api_v1_requisito_previo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequisitoPrevioResponse"},"title":"Response Search Requisito Previo Api V1 Requisito Previo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/requisito-previo/{sysid}":{"get":{"tags":["requisito-previo"],"summary":"Get Requisito Previo By Id","description":"Get a single RequisitoPrevio record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_requisito_previo_by_id_api_v1_requisito_previo__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitoPrevioResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tlcs/":{"get":{"tags":["tlcs"],"summary":"Search Tlcs","description":"Search TLCS records by FRACCION, PAIS and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **pais**: Código de país (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/tlcs?fraccion=8471600101`\n- Solo país: `/tlcs?pais=USA`\n- Solo NICO: `/tlcs?nico=01`\n- Combinaciones: `/tlcs?fraccion=8471600101&pais=USA&nico=01`\n- Todos (con paginación): `/tlcs?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_tlcs_api_v1_tlcs__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"pais","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Código de país a buscar","title":"Pais"},"description":"Código de país a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TLCSResponse"},"title":"Response Search Tlcs Api V1 Tlcs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tlcs/{sysid}/{fraccion}":{"get":{"tags":["tlcs"],"summary":"Get Tlcs By Id","description":"Get a single TLCS record by SYSID and FRACCION (composite key)\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro\n- **fraccion**: Fracción arancelaria","operationId":"get_tlcs_by_id_api_v1_tlcs__sysid___fraccion__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}},{"name":"fraccion","in":"path","required":true,"schema":{"type":"string","title":"Fraccion"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TLCSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-marcas/":{"get":{"tags":["vehiculos-marcas"],"summary":"Search Vehiculos Marcas","description":"Search VehiculosMarcas records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Fracción: `/vehiculos-marcas?fraccion=87032301`\n- Todos (con paginación): `/vehiculos-marcas?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_vehiculos_marcas_api_v1_vehiculos_marcas__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VehiculosMarcasResponse"},"title":"Response Search Vehiculos Marcas Api V1 Vehiculos Marcas Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-marcas/{sysid}":{"get":{"tags":["vehiculos-marcas"],"summary":"Get Vehiculos Marcas By Id","description":"Get a single VehiculosMarcas record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_vehiculos_marcas_by_id_api_v1_vehiculos_marcas__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehiculosMarcasResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-modelos/":{"get":{"tags":["vehiculos-modelos"],"summary":"Search Vehiculos Modelos","description":"Search VehiculosModelos records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Fracción: `/vehiculos-modelos?fraccion=87032301`\n- Todos (con paginación): `/vehiculos-modelos?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_vehiculos_modelos_api_v1_vehiculos_modelos__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VehiculosModelosResponse"},"title":"Response Search Vehiculos Modelos Api V1 Vehiculos Modelos Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-modelos/{sysid}":{"get":{"tags":["vehiculos-modelos"],"summary":"Get Vehiculos Modelos By Id","description":"Get a single VehiculosModelos record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_vehiculos_modelos_by_id_api_v1_vehiculos_modelos__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehiculosModelosResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/regulaciones/":{"get":{"tags":["regulaciones"],"summary":"Search Regulaciones","description":"Search Regulaciones records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/regulaciones?fraccion=8471600101`\n- Solo NICO: `/regulaciones?nico=01`\n- Ambos: `/regulaciones?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/regulaciones?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_regulaciones_api_v1_regulaciones__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RegulacionesResponse"},"title":"Response Search Regulaciones Api V1 Regulaciones Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/regulaciones/{sysid}":{"get":{"tags":["regulaciones"],"summary":"Get Regulaciones By Id","description":"Get a single Regulaciones record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_regulaciones_by_id_api_v1_regulaciones__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegulacionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ieps/":{"get":{"tags":["ieps"],"summary":"Search Ieps","description":"Search IEPS records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (busca en FRACCION_SIN_PUNTO y FRACCION_CON_PUNTO, opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/ieps?fraccion=2207101099`\n- Solo NICO: `/ieps?nico=01`\n- Ambos: `/ieps?fraccion=2207101099&nico=01`\n- Todos (con paginación): `/ieps?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_ieps_api_v1_ieps__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar (busca en ambos campos: con y sin punto)","title":"Fraccion"},"description":"Fracción arancelaria a buscar (busca en ambos campos: con y sin punto)"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IEPSResponse"},"title":"Response Search Ieps Api V1 Ieps Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ieps/{consecutivo}":{"get":{"tags":["ieps"],"summary":"Get Ieps By Id","description":"Get a single IEPS record by CONSECUTIVO\n\n**Requiere autenticación Bearer token**\n\n- **consecutivo**: ID del registro","operationId":"get_ieps_by_id_api_v1_ieps__consecutivo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"consecutivo","in":"path","required":true,"schema":{"type":"integer","title":"Consecutivo"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IEPSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/noms/":{"get":{"tags":["noms"],"summary":"Search Noms","description":"Search Noms records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/noms?fraccion=8471600101`\n- Solo NICO: `/noms?nico=01`\n- Ambos: `/noms?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/noms?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_noms_api_v1_noms__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NomsResponse"},"title":"Response Search Noms Api V1 Noms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/noms/{sysid}":{"get":{"tags":["noms"],"summary":"Get Noms By Id","description":"Get a single Noms record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_noms_by_id_api_v1_noms__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NomsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","description":"Root endpoint","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"Aladi2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":49},{"type":"null"}],"title":"Acuerdo"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt"},"TASANUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasanum"},"TASACALCULADA":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasacalculada"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":499},{"type":"null"}],"title":"Notas"},"OBSERVACIONES":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observaciones"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"Aladi2Response","description":"Schema for Aladi2 response"},"Cuotas2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"PRODUCTO":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Producto"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"DOF2":{"anyOf":[{"type":"string","maxLength":149},{"type":"null"}],"title":"Dof2"},"EMPRESA":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Empresa"},"INCOTERM":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Incoterm"},"DOLARES":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dolares"},"CLAVEMONEDA":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Clavemoneda"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"PORCENTAJE":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Porcentaje"},"DESCRIPCIONDOLARES":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Descripciondolares"},"NOTAS":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notas"},"ESTATUS":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Estatus"},"NUMERORESOLUCION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroresolucion"},"RESOLUCION":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Resolucion"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"Cuotas2Response","description":"Schema for Cuotas2 response"},"CuposResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"NUMEROCUPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numerocupo"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"CONDICION":{"anyOf":[{"type":"string","maxLength":199},{"type":"null"}],"title":"Condicion"},"VIGENCIA":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Vigencia"},"PAIS":{"anyOf":[{"type":"string","maxLength":5},{"type":"null"}],"title":"Pais"},"OBSERVACIONES":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Observaciones"},"ADVIMPO":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Advimpo"},"ADVEXPO":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Advexpo"},"TIPOOPERACION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipooperacion"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"CuposResponse","description":"Schema for Cupos response"},"FraccionesAnterioresResponse":{"properties":{"FRACCIONACTUAL":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionactual"},"FRACCIONANTERIOR":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionanterior"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FraccionesAnterioresResponse","description":"Schema for FraccionesAnteriores response"},"FraccionesResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"FRACCIONPUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionpunto"},"DESCRIPCION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Descripcion"},"UMCLAVE":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Umclave"},"UMABREVIACION":{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}],"title":"Umabreviacion"},"ADVIMPOTXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Advimpotxt"},"ADVIMPONUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advimponum"},"TIPOTASAADVIMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasaadvimpo"},"ADVEXPOTXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Advexpotxt"},"ADVEXPONUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advexponum"},"TIPOTASAADVEXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasaadvexpo"},"TASAIVAFRANJA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaivafranja"},"TASAIVAINTERIOR":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaivainterior"},"TASAISAN":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaisan"},"ARANCELMIXTO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Arancelmixto"},"TASAMIXTA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasamixta"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notas"},"HISTORICO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Historico"},"ARANCELESPECIFICO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Arancelespecifico"},"TIPOVEHICULO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipovehiculo"},"APLICAISAN":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Aplicaisan"},"APLICAIEPS":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Aplicaieps"},"NIVEL":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nivel"},"NICO":{"anyOf":[{"type":"string","maxLength":14},{"type":"null"}],"title":"Nico"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FraccionesResponse","description":"Schema for Fracciones response"},"FraccionesUSAResponse":{"properties":{"FRACCION_SIN_PUNTO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Sin Punto"},"FRACCION_CON_PUNTO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Con Punto"},"FRACCION_MOSTRAR":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Mostrar"},"ESPECIFICO":{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}],"title":"Especifico"},"NIVEL":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nivel"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Descripcion"},"UNIDADCANTIDAD":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Unidadcantidad"},"TARIFA1":{"anyOf":[{"type":"string","maxLength":250},{"type":"null"}],"title":"Tarifa1"},"TLC":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Tlc"},"TARIFA2":{"anyOf":[{"type":"string","maxLength":250},{"type":"null"}],"title":"Tarifa2"},"NOTAS":{"anyOf":[{"type":"string","maxLength":7998},{"type":"null"}],"title":"Notas"},"CONSECUTIVO":{"type":"integer","title":"Consecutivo"}},"type":"object","required":["CONSECUTIVO"],"title":"FraccionesUSAResponse","description":"Schema for FraccionesUSA response"},"FundamentosTLCResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"TASA1NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa1Num"},"TASA2NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa2Num"},"FUNDAMENTO1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento1"},"FUNDAMENTO2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento2"},"PERIODO":{"anyOf":[{"type":"string","maxLength":299},{"type":"null"}],"title":"Periodo"},"MODALIDAD":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Modalidad"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Notas"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FundamentosTLCResponse","description":"Schema for FundamentosTLC response"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IEPSResponse":{"properties":{"FRACCION_SIN_PUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion Sin Punto"},"FRACCION_CON_PUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion Con Punto"},"FUNDAMENTO":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Fundamento"},"CONDICION":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Condicion"},"TASA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa"},"TASAESPECIFICO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaespecifico"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"CONSECUTIVO":{"type":"integer","title":"Consecutivo"}},"type":"object","required":["CONSECUTIVO"],"title":"IEPSResponse","description":"Schema for IEPS response"},"InformacionGeneralResponse":{"properties":{"OBSERVACIONES":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Observaciones"},"NOTAOBSERVACIONES":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Notaobservaciones"},"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"TIPOOPERACION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipooperacion"},"ORDEN":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Orden"},"LEYENDA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Leyenda"},"NOTA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nota"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"InformacionGeneralResponse","description":"Schema for InformacionGeneral response"},"LoginRequest":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"LoginRequest","description":"Schema for login request"},"NomsResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion","default":""},"IMPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Importacion","default":""},"EXPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Exportacion","default":""},"PERMISO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Permiso","default":""},"ACUERDO":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Acuerdo","default":""},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion","default":""},"FUNDAMENTO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento","default":""},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof","default":""},"FORMATONOM":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Formatonom","default":""},"INSTRUCCIONES":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Instrucciones","default":""},"CRITERIO":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Criterio","default":""},"COMPLEMENTO":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Complemento","default":""},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"NomsResponse","description":"Schema for Noms response"},"PaginatedFraccionesResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/FraccionesResponse"},"type":"array","title":"Data"},"total":{"type":"integer","title":"Total","description":"Total de registros que coinciden con la búsqueda"},"page":{"type":"integer","title":"Page","description":"Página actual (base 1)"},"limit":{"type":"integer","title":"Limit","description":"Registros por página"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total de páginas"}},"type":"object","required":["data","total","page","limit","total_pages"],"title":"PaginatedFraccionesResponse","description":"Paginated response wrapper for Fracciones"},"PaginatedFraccionesUSAResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/FraccionesUSAResponse"},"type":"array","title":"Data"},"total":{"type":"integer","title":"Total","description":"Total de registros que coinciden con la búsqueda"},"page":{"type":"integer","title":"Page","description":"Página actual (base 1)"},"limit":{"type":"integer","title":"Limit","description":"Registros por página"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total de páginas"}},"type":"object","required":["data","total","page","limit","total_pages"],"title":"PaginatedFraccionesUSAResponse","description":"Paginated response wrapper for FraccionesUSA"},"PreciosEstimados2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":499},{"type":"null"}],"title":"Descripcion"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"PRECIOESTIMADODLLS":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Precioestimadodlls"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"OBSERVACION1":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion1"},"OBSERVACION2":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion2"},"RESOLUCION":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Resolucion"},"REGLA":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Regla"},"WEB_DOCUMENTO_ID_RES":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id Res"},"WEB_DOCUMENTO_ID_REG":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id Reg"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"PreciosEstimados2Response","description":"Schema for PreciosEstimados2 response"},"ProsecResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion","default":""},"ARTICULO":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Articulo","default":""},"SECTOR":{"anyOf":[{"type":"string","maxLength":6},{"type":"null"}],"title":"Sector","default":""},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt","default":""},"TASANUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasanum","default":"0"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa","default":0},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof","default":""},"OBSERVACION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observacion","default":""},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"ProsecResponse","description":"Schema for Prosec response"},"RCG2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ANEXO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Anexo"},"SECTOR":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sector"},"REGIMEN":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Regimen"},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion"},"ADUANAS":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Aduanas"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"ARCHIVOCRITERIO":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Archivocriterio"},"TIPO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipo"},"IMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impo"},"EXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expo"},"WEB_DOCUMENTO_ID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RCG2Response","description":"Schema for RCG2 response"},"REITResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ARTICULO":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Articulo"},"FUNDAMENTO":{"anyOf":[{"type":"string","maxLength":1500},{"type":"null"}],"title":"Fundamento"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Acuerdo"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"DOCUMENTO":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Documento"},"TEMPORALIDAD":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidad"},"TEMPORALIDADSERVICIO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidadservicio"},"TEMPORALIDADCERTIFICADA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidadcertificada"},"WEB_DOCUMENTO_ID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"REITResponse","description":"Schema for REIT response"},"RegulacionesResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"IMPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Importacion"},"EXPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Exportacion"},"PERMISO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Permiso"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Acuerdo"},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion"},"FUNDAMENTO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"FORMATONOM":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Formatonom"},"INSTRUCCIONES":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Instrucciones"},"CRITERIO":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Criterio"},"COMPLEMENTO":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Complemento"},"CRITERIOESPECIAL":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Criterioespecial"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RegulacionesResponse","description":"Schema for Regulaciones response"},"RequisitoPrevioResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Descripcion"},"OBSERVACION":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"VIGENCIA":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Vigencia"},"NUMEROACUERDO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroacuerdo"},"NUMEROCRITERIO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numerocriterio"},"NUMEROFORMATO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroformato"},"NUMEROINSTRUCCIONES":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroinstrucciones"},"HISTORICO":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Historico"},"NOTA":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Nota"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"IMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impo"},"EXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expo"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RequisitoPrevioResponse","description":"Schema for RequisitoPrevio response"},"TLCSResponse":{"properties":{"FRACCION":{"type":"string","maxLength":10,"title":"Fraccion"},"PAIS":{"type":"string","maxLength":3,"title":"Pais"},"ORDEN":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Orden"},"TASATXT":{"anyOf":[{"type":"string","maxLength":44},{"type":"null"}],"title":"Tasatxt"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"TASA1NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa1Num"},"FACTOR1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Factor1"},"TASA2NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa2Num"},"FACTOR2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Factor2"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Notas"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["FRACCION","PAIS","SYSID"],"title":"TLCSResponse","description":"Schema for TLCS response"},"Token":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type"}},"type":"object","required":["access_token","token_type"],"title":"Token","description":"Schema for token response"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VehiculosMarcasResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"MARCA":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Marca"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"VehiculosMarcasResponse","description":"Schema for VehiculosMarcas response"},"VehiculosModelosResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"MARCA":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Marca"},"MODELO":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Modelo"},"TIPO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipo"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"A20":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A20"},"A19":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A19"},"A18":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A18"},"A17":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A17"},"A16":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A16"},"A15":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A15"},"A14":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A14"},"A13":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A13"},"A12":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A12"},"A11":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A11"},"A10":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A10"},"A9":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A9"},"A8":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A8"},"A7":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A7"},"A6":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A6"},"A5":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A5"},"A4":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A4"},"A3":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A3"},"A2":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A2"},"A1":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A1"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"VehiculosModelosResponse","description":"Schema for VehiculosModelos response"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file diff --git a/backend/api/v1/modules/sitar/prosec/schemas.py b/backend/api/v1/modules/sitar/prosec/schemas.py index 0e798b99..ee69df2f 100644 --- a/backend/api/v1/modules/sitar/prosec/schemas.py +++ b/backend/api/v1/modules/sitar/prosec/schemas.py @@ -1,19 +1,20 @@ -"""PROSEC Schemas""" +"""PROSEC Schemas — aligned with SITAR OpenAPI ProsecResponse.""" from typing import Optional from pydantic import BaseModel, ConfigDict, Field class ProsecResponse(BaseModel): - """PROSEC (Programa de Promoción Sectorial)""" + """PROSEC (Programa de Promoción Sectorial).""" - FRACCION: Optional[str] = Field(None, max_length=10) - PRODUCTO: Optional[str] = Field(None, max_length=999) - TASA: Optional[str] = Field(None, max_length=19) - SECTOR: Optional[str] = Field(None, max_length=2) - ANEXO: Optional[str] = Field(None, max_length=19) - DOF: Optional[str] = Field(None, max_length=8) - NOTAS: Optional[str] = Field(None, max_length=5000) + FRACCION: Optional[str] = Field("", max_length=10) + ARTICULO: Optional[str] = Field("", max_length=3) + SECTOR: Optional[str] = Field("", max_length=6) + TASATXT: Optional[str] = Field("", max_length=19) + TASANUM: Optional[str] = Field(default="0") + TIPOTASA: Optional[int] = Field(default=0) + DOF: Optional[str] = Field("", max_length=8) + OBSERVACION: Optional[str] = "" NICO: Optional[str] = Field("", max_length=2) SYSID: int diff --git a/backend/api/v1/modules/sitar/tlcs/schemas.py b/backend/api/v1/modules/sitar/tlcs/schemas.py index 86a462ed..281bbc48 100644 --- a/backend/api/v1/modules/sitar/tlcs/schemas.py +++ b/backend/api/v1/modules/sitar/tlcs/schemas.py @@ -1,21 +1,23 @@ -"""TLCS Schemas""" +"""TLCS Schemas — aligned with SITAR OpenAPI TLCSResponse.""" from typing import Optional from pydantic import BaseModel, ConfigDict, Field class TLCSResponse(BaseModel): - """TLCS (Tratados de Libre Comercio) response model""" + """TLCS (Tratados de Libre Comercio) response model.""" - FRACCION: Optional[str] = Field(None, max_length=8) - PAIS: Optional[str] = Field(None, max_length=3) - TASATXT: Optional[str] = Field(None, max_length=19) - TASANUM: Optional[str] = None - TASACALCULADA: Optional[str] = Field(None, max_length=19) - TLC: Optional[str] = Field(None, max_length=6) - NOTA: Optional[str] = None + FRACCION: str = Field(max_length=10) + PAIS: str = Field(max_length=3) + ORDEN: Optional[int] = None + TASATXT: Optional[str] = Field(None, max_length=44) + TIPOTASA: Optional[int] = None + TASA1NUM: Optional[str] = None + FACTOR1: Optional[str] = None + TASA2NUM: Optional[str] = None + FACTOR2: Optional[str] = None DOF: Optional[str] = Field(None, max_length=8) - OBSERVACION: Optional[str] = None + NOTAS: Optional[str] = Field(None, max_length=999) NICO: Optional[str] = Field("", max_length=2) SYSID: int diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index f9379f5a..24c21b6d 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -2,7 +2,7 @@ import os from celery import Celery from celery.signals import task_postrun, task_prerun -from core.database import rls_company_var, rls_tenant_var +from core.database import reset_rls_context_tokens, rls_company_var, rls_tenant_var valkey_url = os.getenv("VALKEY_URL", "redis://valkey:6379/0") @@ -60,14 +60,8 @@ def _reset_rls_context_from_task(task_id=None, task=None, **_): if tokens is None: return token_t, token_c = tokens - try: - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) - except ValueError: - rls_tenant_var.set(None) - rls_company_var.set(None) - finally: - delattr(task, _RLS_TOKENS_ATTR) + reset_rls_context_tokens(token_t, token_c) + delattr(task, _RLS_TOKENS_ATTR) # ---------------------------------------------------------------------------- # Import models in correct order for SQLAlchemy relationship resolution diff --git a/backend/core/database.py b/backend/core/database.py index 95e937d3..f794f11a 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -114,6 +114,23 @@ def set_rls_context( _apply_rls_context(session.connection(), tenant_id, company_id) +def reset_rls_context_tokens(token_t, token_c) -> None: + """Restaura ContextVars de RLS de forma segura entre hilos/tareas asyncio. + + ``ContextVar.reset`` exige que el token se cree y restaure en el mismo contexto + lógico; en rutas FastAPI async + dependencias síncronas con ``yield`` (thread + pool) el ``finally`` puede ejecutarse en otro contexto y lanzar ``ValueError`` + (mensaje: "was created in a different Context"). En ese caso degradamos a + ``set(None)``, igual que ``task_postrun`` en ``core/celery_app.py``. + """ + try: + rls_tenant_var.reset(token_t) + rls_company_var.reset(token_c) + except (ValueError, RuntimeError): + rls_tenant_var.set(None) + rls_company_var.set(None) + + def _extract_rls_context(request: Optional[Request]) -> tuple[Optional[int], Optional[int]]: """Recupera ``tenant_id`` / ``company_id`` del estado del request (o de cookies).""" if request is None: @@ -150,8 +167,7 @@ def get_core_db(request: Request = None) -> Generator[Session, None, None]: yield db finally: db.close() - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) + reset_rls_context_tokens(token_t, token_c) async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSession, None]: @@ -168,8 +184,7 @@ async def get_async_core_db(request: Request = None) -> AsyncGenerator[AsyncSess finally: await session.close() finally: - rls_tenant_var.reset(token_t) - rls_company_var.reset(token_c) + reset_rls_context_tokens(token_t, token_c) @contextmanager diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 654feadf..f6150a23 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -159,118 +159,14 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): hub_headers["X-Tenant-Override"] = str(tenant_override) logger.info("[license] tenant override propagated to Hub: %s", tenant_override) + # Solo la petición HTTP al Hub va en try: los errores de rutas (p. ej. ContextVar RLS) + # deben propagarse y no etiquetarse como fallo de licencia. try: - # Validación contra el Hub Central async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get( f"{settings.HUB_URL}api/v1/auth/verify-license", headers=hub_headers ) - - logger.info(f"🔑 verify-license → status={response.status_code} body={response.text[:300]}") - - if response.status_code == 404: - # Endpoint no existe en este Hub — dejar pasar - return await call_next(request) - - if response.status_code == 200: - data = response.json() - - # Escenario 1: sin licencia asignada o licencia inactiva - if not data.get("valid", False): - message = data.get("message", "Sin licencia asignada para este tenant") - detail = data.get("detail") - reason = data.get("reason") - # Si el Hub reporta token inválido/expirado, devolver 401 para que - # el frontend dispare el auto-refresh (solo se activa con 401/403, no 402). - if _is_token_issue_message(message, detail, reason): - logger.warning( - "[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s", - message, - detail, - reason, - ) - return JSONResponse( - status_code=401, - content={ - "error": "TOKEN_EXPIRED", - "message": message, - "status_code": 401, - } - ) - - logger.warning( - "[license] licencia invalida para tenant=%s | message=%s", - data.get("tenant_slug"), - message, - ) - return JSONResponse( - status_code=402, - content={ - "error": "LICENSE_ERROR", - "message": message, - "status_code": 402, - } - ) - - # Escenario 2: licencia vencida (verificación local de expires_at) - expires_at_str = data.get("expires_at") - if expires_at_str: - try: - expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) - if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - if expires_at < datetime.now(timezone.utc): - logger.warning( - "[license] licencia expirada para tenant=%s | expires_at=%s", - data.get("tenant_slug"), - expires_at_str, - ) - return JSONResponse( - status_code=402, - content={ - "error": "LICENSE_EXPIRED", - "message": f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción.", - "status_code": 402, - } - ) - except (ValueError, TypeError): - pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad - - request.state.license_info = data - return await call_next(request) # <--- Único camino al éxito - - elif response.status_code == 401: - logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)") - return JSONResponse( - status_code=401, - content={ - "error": "TOKEN_EXPIRED", - "message": "Token inválido o expirado.", - "status_code": 401, - } - ) - - elif response.status_code == 403: - return JSONResponse( - status_code=403, - content={ - "error": "FORBIDDEN", - "message": "El Tenant no tiene permisos en el Hub central.", - "status_code": 403, - } - ) - else: - logger.error(f"Hub error status: {response.status_code}") - return JSONResponse( - status_code=503, - content={ - "error": "HUB_ERROR", - "message": "Error en el servidor de licencias.", - "status_code": 503, - } - ) - except (httpx.ConnectError, httpx.TimeoutException) as e: logger.critical(f"❌ CRITICAL: Hub unreachable: {str(e)}") return JSONResponse( @@ -282,12 +178,131 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): } ) except Exception as e: - logger.error(f"Unexpected license error: {str(e)}") + logger.error(f"Hub verify-license request failed: {str(e)}") return JSONResponse( status_code=500, - content={"error": "VALIDATION_ERROR", "message": "Error interno de validación.", "status_code": 500} + content={ + "error": "VALIDATION_ERROR", + "message": "Error interno al validar licencia con el Hub.", + "status_code": 500, + } ) + logger.info(f"🔑 verify-license → status={response.status_code} body={response.text[:300]}") + + if response.status_code == 404: + # Endpoint no existe en este Hub — dejar pasar + return await call_next(request) + + if response.status_code == 200: + try: + data = response.json() + except Exception as e: + logger.error(f"Hub verify-license JSON parse failed: {str(e)}") + return JSONResponse( + status_code=503, + content={ + "error": "HUB_ERROR", + "message": "Respuesta inválida del servidor de licencias.", + "status_code": 503, + } + ) + + # Escenario 1: sin licencia asignada o licencia inactiva + if not data.get("valid", False): + message = data.get("message", "Sin licencia asignada para este tenant") + detail = data.get("detail") + reason = data.get("reason") + # Si el Hub reporta token inválido/expirado, devolver 401 para que + # el frontend dispare el auto-refresh (solo se activa con 401/403, no 402). + if _is_token_issue_message(message, detail, reason): + logger.warning( + "[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s", + message, + detail, + reason, + ) + return JSONResponse( + status_code=401, + content={ + "error": "TOKEN_EXPIRED", + "message": message, + "status_code": 401, + } + ) + + logger.warning( + "[license] licencia invalida para tenant=%s | message=%s", + data.get("tenant_slug"), + message, + ) + return JSONResponse( + status_code=402, + content={ + "error": "LICENSE_ERROR", + "message": message, + "status_code": 402, + } + ) + + # Escenario 2: licencia vencida (verificación local de expires_at) + expires_at_str = data.get("expires_at") + if expires_at_str: + try: + expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + if expires_at < datetime.now(timezone.utc): + logger.warning( + "[license] licencia expirada para tenant=%s | expires_at=%s", + data.get("tenant_slug"), + expires_at_str, + ) + return JSONResponse( + status_code=402, + content={ + "error": "LICENSE_EXPIRED", + "message": f"La licencia venció el {expires_at.strftime('%d/%m/%Y')}. Renueva tu suscripción.", + "status_code": 402, + } + ) + except (ValueError, TypeError): + pass # Si no se puede parsear, dejamos pasar — el Hub es la fuente de verdad + + request.state.license_info = data + return await call_next(request) + + if response.status_code == 401: + logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)") + return JSONResponse( + status_code=401, + content={ + "error": "TOKEN_EXPIRED", + "message": "Token inválido o expirado.", + "status_code": 401, + } + ) + + if response.status_code == 403: + return JSONResponse( + status_code=403, + content={ + "error": "FORBIDDEN", + "message": "El Tenant no tiene permisos en el Hub central.", + "status_code": 403, + } + ) + + logger.error(f"Hub error status: {response.status_code}") + return JSONResponse( + status_code=503, + content={ + "error": "HUB_ERROR", + "message": "Error en el servidor de licencias.", + "status_code": 503, + } + ) + class RequestLoggingMiddleware(BaseHTTPMiddleware): """ diff --git a/frontend/src/lib/api/dashboard/a76/sitar.ts b/frontend/src/lib/api/dashboard/a76/sitar.ts index 9692ab5a..a25dbcf0 100644 --- a/frontend/src/lib/api/dashboard/a76/sitar.ts +++ b/frontend/src/lib/api/dashboard/a76/sitar.ts @@ -1,41 +1,42 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; +/** Matches SITAR OpenAPI TLCSResponse */ export interface SitarTLCS { - FRACCION: string; - PAIS: string; - TASATXT: string; - TASANUM?: string | null; - TLC: string; - NOTA: string | null; - DOF: string | null; - OBSERVACION?: string | null; - NICO?: string; - SYSID: number; + FRACCION: string; + PAIS: string; + TASATXT?: string | null; + TASA1NUM?: string | null; + TASA2NUM?: string | null; + DOF?: string | null; + NOTAS?: string | null; + NICO?: string; + SYSID: number; } +/** Matches SITAR OpenAPI ProsecResponse */ export interface SitarPROSEC { - FRACCION: string; - PRODUCTO: string; - TASA: string; - SECTOR: string; - ANEXO: string; - DOF: string; - NOTAS: string | null; - NICO?: string; - SYSID: number; + FRACCION?: string; + ARTICULO?: string; + SECTOR?: string; + TASATXT?: string; + TASANUM?: string | null; + DOF?: string | null; + OBSERVACION?: string | null; + NICO?: string; + SYSID: number; } export interface SitarALADI { - FRACCION: string; - ACUERDO: string; - PAIS: string; - TASATXT: string; - TASANUM?: string | null; - DOF: string; - NOTAS: string | null; - NICO?: string; - SYSID: number; + FRACCION: string; + ACUERDO: string; + PAIS: string; + TASATXT: string; + TASANUM?: string | null; + DOF: string; + NOTAS: string | null; + NICO?: string; + SYSID: number; } export async function getSitarTLCS(filters: { fraccion: string; nico?: string }): Promise> { diff --git a/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte b/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte index ef9b8327..a054deab 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte @@ -272,7 +272,7 @@ {item.PAIS} {item.TASATXT} {item.DOF || '-'} - {item.NOTA || '-'} + {item.NOTAS || '-'} {:else} No hay información de TLCS disponible para esta fracción. @@ -303,9 +303,9 @@ {#each sitarPROSECData as item} - {item.PRODUCTO} + {item.ARTICULO} {item.SECTOR} - {item.TASA} + {item.TASATXT} {item.DOF || '-'} {:else} diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index a9ce69ac..3deb0c4a 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -3620,9 +3620,9 @@ {item.DOF || '-'} - {item.NOTA || '-'} + {item.NOTAS || '-'} {:else} @@ -3676,14 +3676,14 @@ {item.PRODUCTO}{item.ARTICULO} {item.SECTOR} {item.TASA}{item.TASATXT} {item.DOF || '-'} From 6113c47839a56f6b0aca80dd9908460c14550bc8 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 8 May 2026 14:12:31 -0500 Subject: [PATCH 3/7] Add BaseAPIException handling to invoice routes for improved error management --- backend/api/v1/modules/a76/invoices/routes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/api/v1/modules/a76/invoices/routes.py b/backend/api/v1/modules/a76/invoices/routes.py index f5428197..54fb72d0 100644 --- a/backend/api/v1/modules/a76/invoices/routes.py +++ b/backend/api/v1/modules/a76/invoices/routes.py @@ -3,6 +3,7 @@ from typing import Dict, Any, Optional from core.config import settings from core.database import get_core_db +from core.exceptions import BaseAPIException from core.security import collect_user_role_names, get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query, Path from sqlalchemy import func, or_, and_ @@ -69,6 +70,8 @@ def get_creation_data( return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id) except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("get_creation_data failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al cargar datos de creación: {str(e)}") @@ -97,6 +100,8 @@ def get_edition_data( return data except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("get_edition_data failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al cargar datos de edición: {str(e)}") @@ -191,6 +196,8 @@ def create_invoice( return services.InvoiceService.create(db, data, tenant_id, company_id) except HTTPException: raise + except BaseAPIException: + raise except Exception as e: logger.exception("create_invoice failed: %s", e) raise HTTPException(status_code=500, detail=f"Error al guardar factura: {str(e)}") @@ -337,6 +344,8 @@ def list_invoices( "page": page, "page_size": page_size } + except BaseAPIException: + raise except Exception as e: logger.exception("list_invoices failed: %s", e) raise HTTPException(status_code=500, detail=f"Internal server error in invoices list: {str(e)}") From 8cd6e19a6a6e40d2a0e1e76f0442de72a040a22a Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 8 May 2026 14:43:07 -0600 Subject: [PATCH 4/7] feature/api-sittar-actualizacion --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7b232c1b..28b35572 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -313,5 +313,5 @@ networks: driver: bridge hub-net: - external: true + external: false name: aduanasoft-hub_default From f9f87a5e0caea2859c528c58d5ee07f00d5f3145 Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 8 May 2026 14:51:02 -0600 Subject: [PATCH 5/7] Remove SITAR external OpenAPI snapshot and README --- backend/api/v1/modules/sitar/openapi/README.md | 10 ---------- .../modules/sitar/openapi/sitar-api-openapi-3.1.json | 1 - 2 files changed, 11 deletions(-) delete mode 100644 backend/api/v1/modules/sitar/openapi/README.md delete mode 100644 backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json diff --git a/backend/api/v1/modules/sitar/openapi/README.md b/backend/api/v1/modules/sitar/openapi/README.md deleted file mode 100644 index 25a68fac..00000000 --- a/backend/api/v1/modules/sitar/openapi/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# SITAR API — especificación OpenAPI rescatada - -| Campo | Valor | -|-------|--------| -| Fuente | `GET http://api.sitar.aduanasoft.com:880/fractions/api/v1/openapi.json` | -| UI Swagger | `http://api.sitar.aduanasoft.com:880/fractions/docs` | -| Rescatado | 2026-05-08 | -| Archivo | [sitar-api-openapi-3.1.json](sitar-api-openapi-3.1.json) (OpenAPI 3.1, «SITAR API - DEV») | - -Las rutas documentadas usan prefijo `/api/v1/...`. Las peticiones desde este proyecto anteponen `{SITAR_API_URL}/fractions/` (ver `SitarAPIBaseService`), por ejemplo: `{SITAR_API_URL}/fractions/api/v1/tlcs/`. diff --git a/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json b/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json deleted file mode 100644 index 8437d53a..00000000 --- a/backend/api/v1/modules/sitar/openapi/sitar-api-openapi-3.1.json +++ /dev/null @@ -1 +0,0 @@ -{"openapi":"3.1.0","info":{"title":"SITAR API - DEV","description":"FastAPI application with SQL Server integration","version":"1.0.0"},"paths":{"/api/v1/auth/login":{"post":{"tags":["authentication"],"summary":"Login","description":"Login endpoint - Authenticate user and return JWT token\n\nArgs:\n credentials: Login credentials (username and password)\n \nReturns:\n Access token (Bearer token)\n \nRaises:\n HTTPException: If credentials are invalid","operationId":"login_api_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Token"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["authentication"],"summary":"Get Current User Info","description":"Get current authenticated user information\nEndpoint protegido que requiere un token válido\n\nArgs:\n current_user: Username extracted from JWT token\n \nReturns:\n Current user information","operationId":"get_current_user_info_api_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/aladi2/":{"get":{"tags":["aladi2"],"summary":"Search Aladi2","description":"Search Aladi2 records by FRACCION and/or NICO\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/aladi2?fraccion=8471600101`\n- Solo NICO: `/aladi2?nico=01`\n- Ambos: `/aladi2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/aladi2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_aladi2_api_v1_aladi2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Aladi2Response"},"title":"Response Search Aladi2 Api V1 Aladi2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/aladi2/{sysid}":{"get":{"tags":["aladi2"],"summary":"Get Aladi2 By Id","description":"Get a single Aladi2 record by SYSID\n\n- **sysid**: ID del registro","operationId":"get_aladi2_by_id_api_v1_aladi2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Aladi2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cuotas2/":{"get":{"tags":["cuotas2"],"summary":"Search Cuotas2","description":"Search Cuotas2 records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/cuotas2?fraccion=8471600101`\n- Solo NICO: `/cuotas2?nico=01`\n- Ambos: `/cuotas2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/cuotas2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_cuotas2_api_v1_cuotas2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Cuotas2Response"},"title":"Response Search Cuotas2 Api V1 Cuotas2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cuotas2/{sysid}":{"get":{"tags":["cuotas2"],"summary":"Get Cuotas2 By Id","description":"Get a single Cuotas2 record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_cuotas2_by_id_api_v1_cuotas2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Cuotas2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cupos/":{"get":{"tags":["cupos"],"summary":"Search Cupos","description":"Search Cupos records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/cupos?fraccion=8471600101`\n- Solo NICO: `/cupos?nico=01`\n- Ambos: `/cupos?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/cupos?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_cupos_api_v1_cupos__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuposResponse"},"title":"Response Search Cupos Api V1 Cupos Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/cupos/{sysid}":{"get":{"tags":["cupos"],"summary":"Get Cupos By Id","description":"Get a single Cupos record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_cupos_by_id_api_v1_cupos__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CuposResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones/":{"get":{"tags":["fracciones"],"summary":"Search Fracciones","description":"Search Fracciones records by FRACCION and/or NICO\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **nivel**: Nivel de la fracción (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fracciones?fraccion=8471600101`\n- Solo NICO: `/fracciones?nico=01`\n- Solo nivel: `/fracciones?nivel=4`\n- Ambos: `/fracciones?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/fracciones?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_api_v1_fracciones__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"nivel","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Nivel de la fracción (opcional)","title":"Nivel"},"description":"Nivel de la fracción (opcional)"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedFraccionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones/{sysid}":{"get":{"tags":["fracciones"],"summary":"Get Fracciones By Id","description":"Get a single Fracciones record by SYSID\n\n- **sysid**: ID del registro","operationId":"get_fracciones_by_id_api_v1_fracciones__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-usa/":{"get":{"tags":["fracciones-usa"],"summary":"Search Fracciones Usa","description":"Search Fracciones USA records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (busca en FRACCION_SIN_PUNTO, FRACCION_CON_PUNTO y FRACCION_MOSTRAR)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nEjemplos:\n- `/fracciones-usa?fraccion=8471600101`\n- `/fracciones-usa?fraccion=8471.60.01.01`\n- `/fracciones-usa?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_usa_api_v1_fracciones_usa__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedFraccionesUSAResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-usa/{consecutivo}":{"get":{"tags":["fracciones-usa"],"summary":"Get Fracciones Usa By Id","description":"Get a single Fracciones USA record by CONSECUTIVO\n\n**Requiere autenticación Bearer token**\n\n- **consecutivo**: ID del registro","operationId":"get_fracciones_usa_by_id_api_v1_fracciones_usa__consecutivo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"consecutivo","in":"path","required":true,"schema":{"type":"integer","title":"Consecutivo"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesUSAResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-anteriores/":{"get":{"tags":["fracciones-anteriores"],"summary":"Search Fracciones Anteriores","description":"Search Fracciones Anteriores records by FRACCIONACTUAL and/or FRACCIONANTERIOR\n\n**Requiere autenticación Bearer token**\n\n- **fraccion_actual**: Fracción actual (opcional)\n- **fraccion_anterior**: Fracción anterior (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción actual: `/fracciones-anteriores?fraccion_actual=8471600101`\n- Solo fracción anterior: `/fracciones-anteriores?fraccion_anterior=8471600101`\n- Ambas: `/fracciones-anteriores?fraccion_actual=8471600101&fraccion_anterior=8471600102`\n- Todos (con paginación): `/fracciones-anteriores?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fracciones_anteriores_api_v1_fracciones_anteriores__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion_actual","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción actual a buscar","title":"Fraccion Actual"},"description":"Fracción actual a buscar"},{"name":"fraccion_anterior","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción anterior a buscar","title":"Fraccion Anterior"},"description":"Fracción anterior a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FraccionesAnterioresResponse"},"title":"Response Search Fracciones Anteriores Api V1 Fracciones Anteriores Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fracciones-anteriores/{sysid}":{"get":{"tags":["fracciones-anteriores"],"summary":"Get Fracciones Anteriores By Id","description":"Get a single Fracciones Anteriores record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_fracciones_anteriores_by_id_api_v1_fracciones_anteriores__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FraccionesAnterioresResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/":{"get":{"tags":["fundamentos-tlc"],"summary":"Search Fundamentos Tlc","description":"Search Fundamentos TLC records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fundamentos-tlc?fraccion=84716001`\n- Solo NICO: `/fundamentos-tlc?nico=01`\n- Ambos: `/fundamentos-tlc?fraccion=84716001&nico=01`\n- Todos (con paginación): `/fundamentos-tlc?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_fundamentos_tlc_api_v1_fundamentos_tlc__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundamentosTLCResponse"},"title":"Response Search Fundamentos Tlc Api V1 Fundamentos Tlc Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/tipat":{"get":{"tags":["fundamentos-tlc"],"summary":"Search Fundamentos Tlc Tipat","description":"Search Fundamentos TLC records by FRACCION and/or NICO where FUNDAMENTO2 = 'TIPAT'\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/fundamentos-tlc/tipat?fraccion=84716001`\n- Solo NICO: `/fundamentos-tlc/tipat?nico=01`\n- Ambos: `/fundamentos-tlc/tipat?fraccion=84716001&nico=01`\n- Todos con TIPAT (con paginación): `/fundamentos-tlc/tipat?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)\n\nTodos los resultados tendrán FUNDAMENTO2 = 'TIPAT'","operationId":"search_fundamentos_tlc_tipat_api_v1_fundamentos_tlc_tipat_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundamentosTLCResponse"},"title":"Response Search Fundamentos Tlc Tipat Api V1 Fundamentos Tlc Tipat Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/fundamentos-tlc/{sysid}":{"get":{"tags":["fundamentos-tlc"],"summary":"Get Fundamentos Tlc By Id","description":"Get a single Fundamentos TLC record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_fundamentos_tlc_by_id_api_v1_fundamentos_tlc__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FundamentosTLCResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/informacion-general/":{"get":{"tags":["informacion-general"],"summary":"Search Informacion General","description":"Search Informacion General records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/informacion-general?fraccion=8471600101`\n- Solo NICO: `/informacion-general?nico=01`\n- Ambos: `/informacion-general?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/informacion-general?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_informacion_general_api_v1_informacion_general__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InformacionGeneralResponse"},"title":"Response Search Informacion General Api V1 Informacion General Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/informacion-general/{sysid}":{"get":{"tags":["informacion-general"],"summary":"Get Informacion General By Id","description":"Get a single Informacion General record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_informacion_general_by_id_api_v1_informacion_general__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InformacionGeneralResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/precios-estimados2/":{"get":{"tags":["precios-estimados2"],"summary":"Search Precios Estimados2","description":"Search Precios Estimados records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/precios-estimados2?fraccion=8471600101`\n- Solo NICO: `/precios-estimados2?nico=01`\n- Ambos: `/precios-estimados2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/precios-estimados2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_precios_estimados2_api_v1_precios_estimados2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PreciosEstimados2Response"},"title":"Response Search Precios Estimados2 Api V1 Precios Estimados2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/precios-estimados2/{sysid}":{"get":{"tags":["precios-estimados2"],"summary":"Get Precios Estimados2 By Id","description":"Get a single Precios Estimados record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_precios_estimados2_by_id_api_v1_precios_estimados2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreciosEstimados2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/prosec/":{"get":{"tags":["prosec"],"summary":"Search Prosec","description":"Search Prosec records by FRACCION, NICO, SECTOR and/or ARTICULO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **sector**: Sector (opcional)\n- **articulo**: Artículo (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/prosec?fraccion=8471600101`\n- Solo NICO: `/prosec?nico=01`\n- Solo sector: `/prosec?sector=01`\n- Solo artículo: `/prosec?articulo=001`\n- Combinaciones: `/prosec?fraccion=8471600101§or=01&articulo=001`\n- Todos (con paginación): `/prosec?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_prosec_api_v1_prosec__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"sector","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Sector a buscar","title":"Sector"},"description":"Sector a buscar"},{"name":"articulo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Artículo a buscar","title":"Articulo"},"description":"Artículo a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProsecResponse"},"title":"Response Search Prosec Api V1 Prosec Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/prosec/{sysid}":{"get":{"tags":["prosec"],"summary":"Get Prosec By Id","description":"Get a single Prosec record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_prosec_by_id_api_v1_prosec__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProsecResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rcg2/":{"get":{"tags":["rcg2"],"summary":"Search Rcg2","description":"Search RCG2 records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/rcg2?fraccion=8471600101`\n- Solo NICO: `/rcg2?nico=01`\n- Ambos: `/rcg2?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/rcg2?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_rcg2_api_v1_rcg2__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RCG2Response"},"title":"Response Search Rcg2 Api V1 Rcg2 Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/rcg2/{sysid}":{"get":{"tags":["rcg2"],"summary":"Get Rcg2 By Id","description":"Get a single RCG2 record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_rcg2_by_id_api_v1_rcg2__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RCG2Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reit/":{"get":{"tags":["reit"],"summary":"Search Reit","description":"Search REIT records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/reit?fraccion=8471600101`\n- Solo NICO: `/reit?nico=01`\n- Ambos: `/reit?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/reit?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_reit_api_v1_reit__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/REITResponse"},"title":"Response Search Reit Api V1 Reit Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reit/{sysid}":{"get":{"tags":["reit"],"summary":"Get Reit By Id","description":"Get a single REIT record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_reit_by_id_api_v1_reit__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/REITResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/requisito-previo/":{"get":{"tags":["requisito-previo"],"summary":"Search Requisito Previo","description":"Search RequisitoPrevio records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/requisito-previo?fraccion=84716001`\n- Solo NICO: `/requisito-previo?nico=01`\n- Ambos: `/requisito-previo?fraccion=84716001&nico=01`\n- Todos (con paginación): `/requisito-previo?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_requisito_previo_api_v1_requisito_previo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequisitoPrevioResponse"},"title":"Response Search Requisito Previo Api V1 Requisito Previo Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/requisito-previo/{sysid}":{"get":{"tags":["requisito-previo"],"summary":"Get Requisito Previo By Id","description":"Get a single RequisitoPrevio record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_requisito_previo_by_id_api_v1_requisito_previo__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitoPrevioResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tlcs/":{"get":{"tags":["tlcs"],"summary":"Search Tlcs","description":"Search TLCS records by FRACCION, PAIS and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **pais**: Código de país (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/tlcs?fraccion=8471600101`\n- Solo país: `/tlcs?pais=USA`\n- Solo NICO: `/tlcs?nico=01`\n- Combinaciones: `/tlcs?fraccion=8471600101&pais=USA&nico=01`\n- Todos (con paginación): `/tlcs?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_tlcs_api_v1_tlcs__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"pais","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Código de país a buscar","title":"Pais"},"description":"Código de país a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TLCSResponse"},"title":"Response Search Tlcs Api V1 Tlcs Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tlcs/{sysid}/{fraccion}":{"get":{"tags":["tlcs"],"summary":"Get Tlcs By Id","description":"Get a single TLCS record by SYSID and FRACCION (composite key)\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro\n- **fraccion**: Fracción arancelaria","operationId":"get_tlcs_by_id_api_v1_tlcs__sysid___fraccion__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}},{"name":"fraccion","in":"path","required":true,"schema":{"type":"string","title":"Fraccion"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TLCSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-marcas/":{"get":{"tags":["vehiculos-marcas"],"summary":"Search Vehiculos Marcas","description":"Search VehiculosMarcas records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Fracción: `/vehiculos-marcas?fraccion=87032301`\n- Todos (con paginación): `/vehiculos-marcas?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_vehiculos_marcas_api_v1_vehiculos_marcas__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VehiculosMarcasResponse"},"title":"Response Search Vehiculos Marcas Api V1 Vehiculos Marcas Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-marcas/{sysid}":{"get":{"tags":["vehiculos-marcas"],"summary":"Get Vehiculos Marcas By Id","description":"Get a single VehiculosMarcas record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_vehiculos_marcas_by_id_api_v1_vehiculos_marcas__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehiculosMarcasResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-modelos/":{"get":{"tags":["vehiculos-modelos"],"summary":"Search Vehiculos Modelos","description":"Search VehiculosModelos records by FRACCION\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Fracción: `/vehiculos-modelos?fraccion=87032301`\n- Todos (con paginación): `/vehiculos-modelos?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_vehiculos_modelos_api_v1_vehiculos_modelos__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VehiculosModelosResponse"},"title":"Response Search Vehiculos Modelos Api V1 Vehiculos Modelos Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/vehiculos-modelos/{sysid}":{"get":{"tags":["vehiculos-modelos"],"summary":"Get Vehiculos Modelos By Id","description":"Get a single VehiculosModelos record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_vehiculos_modelos_by_id_api_v1_vehiculos_modelos__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehiculosModelosResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/regulaciones/":{"get":{"tags":["regulaciones"],"summary":"Search Regulaciones","description":"Search Regulaciones records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/regulaciones?fraccion=8471600101`\n- Solo NICO: `/regulaciones?nico=01`\n- Ambos: `/regulaciones?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/regulaciones?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_regulaciones_api_v1_regulaciones__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RegulacionesResponse"},"title":"Response Search Regulaciones Api V1 Regulaciones Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/regulaciones/{sysid}":{"get":{"tags":["regulaciones"],"summary":"Get Regulaciones By Id","description":"Get a single Regulaciones record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_regulaciones_by_id_api_v1_regulaciones__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegulacionesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ieps/":{"get":{"tags":["ieps"],"summary":"Search Ieps","description":"Search IEPS records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (busca en FRACCION_SIN_PUNTO y FRACCION_CON_PUNTO, opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/ieps?fraccion=2207101099`\n- Solo NICO: `/ieps?nico=01`\n- Ambos: `/ieps?fraccion=2207101099&nico=01`\n- Todos (con paginación): `/ieps?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_ieps_api_v1_ieps__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar (busca en ambos campos: con y sin punto)","title":"Fraccion"},"description":"Fracción arancelaria a buscar (busca en ambos campos: con y sin punto)"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/IEPSResponse"},"title":"Response Search Ieps Api V1 Ieps Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ieps/{consecutivo}":{"get":{"tags":["ieps"],"summary":"Get Ieps By Id","description":"Get a single IEPS record by CONSECUTIVO\n\n**Requiere autenticación Bearer token**\n\n- **consecutivo**: ID del registro","operationId":"get_ieps_by_id_api_v1_ieps__consecutivo__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"consecutivo","in":"path","required":true,"schema":{"type":"integer","title":"Consecutivo"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IEPSResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/noms/":{"get":{"tags":["noms"],"summary":"Search Noms","description":"Search Noms records by FRACCION and/or NICO\n\n**Requiere autenticación Bearer token**\n\n- **fraccion**: Fracción arancelaria (opcional)\n- **nico**: NICO (opcional)\n- **skip**: Paginación - registros a saltar (default: 0)\n- **limit**: Paginación - máximo de registros (default: 100, max: 1000)\n\nPuedes buscar por:\n- Solo fracción: `/noms?fraccion=8471600101`\n- Solo NICO: `/noms?nico=01`\n- Ambos: `/noms?fraccion=8471600101&nico=01`\n- Todos (con paginación): `/noms?limit=50`\n\nDevuelve una lista que puede contener:\n- 0 elementos (sin resultados)\n- 1 elemento (resultado único)\n- N elementos (múltiples resultados)","operationId":"search_noms_api_v1_noms__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"fraccion","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Fracción arancelaria a buscar","title":"Fraccion"},"description":"Fracción arancelaria a buscar"},{"name":"nico","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"NICO a buscar","title":"Nico"},"description":"NICO a buscar"},{"name":"skip","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Número de registros a saltar","default":0,"title":"Skip"},"description":"Número de registros a saltar"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Máximo de registros a devolver","default":100,"title":"Limit"},"description":"Máximo de registros a devolver"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NomsResponse"},"title":"Response Search Noms Api V1 Noms Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/noms/{sysid}":{"get":{"tags":["noms"],"summary":"Get Noms By Id","description":"Get a single Noms record by SYSID\n\n**Requiere autenticación Bearer token**\n\n- **sysid**: ID del registro","operationId":"get_noms_by_id_api_v1_noms__sysid__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"sysid","in":"path","required":true,"schema":{"type":"integer","title":"Sysid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NomsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","description":"Root endpoint","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"Aladi2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":49},{"type":"null"}],"title":"Acuerdo"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt"},"TASANUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasanum"},"TASACALCULADA":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasacalculada"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":499},{"type":"null"}],"title":"Notas"},"OBSERVACIONES":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observaciones"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"Aladi2Response","description":"Schema for Aladi2 response"},"Cuotas2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"PRODUCTO":{"anyOf":[{"type":"string","maxLength":300},{"type":"null"}],"title":"Producto"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"DOF2":{"anyOf":[{"type":"string","maxLength":149},{"type":"null"}],"title":"Dof2"},"EMPRESA":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Empresa"},"INCOTERM":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Incoterm"},"DOLARES":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dolares"},"CLAVEMONEDA":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Clavemoneda"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"PORCENTAJE":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Porcentaje"},"DESCRIPCIONDOLARES":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Descripciondolares"},"NOTAS":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notas"},"ESTATUS":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Estatus"},"NUMERORESOLUCION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroresolucion"},"RESOLUCION":{"anyOf":[{"type":"string","maxLength":150},{"type":"null"}],"title":"Resolucion"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"Cuotas2Response","description":"Schema for Cuotas2 response"},"CuposResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"NUMEROCUPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numerocupo"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"CONDICION":{"anyOf":[{"type":"string","maxLength":199},{"type":"null"}],"title":"Condicion"},"VIGENCIA":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Vigencia"},"PAIS":{"anyOf":[{"type":"string","maxLength":5},{"type":"null"}],"title":"Pais"},"OBSERVACIONES":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Observaciones"},"ADVIMPO":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Advimpo"},"ADVEXPO":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Advexpo"},"TIPOOPERACION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipooperacion"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"CuposResponse","description":"Schema for Cupos response"},"FraccionesAnterioresResponse":{"properties":{"FRACCIONACTUAL":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionactual"},"FRACCIONANTERIOR":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionanterior"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FraccionesAnterioresResponse","description":"Schema for FraccionesAnteriores response"},"FraccionesResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"FRACCIONPUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccionpunto"},"DESCRIPCION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Descripcion"},"UMCLAVE":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Umclave"},"UMABREVIACION":{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}],"title":"Umabreviacion"},"ADVIMPOTXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Advimpotxt"},"ADVIMPONUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advimponum"},"TIPOTASAADVIMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasaadvimpo"},"ADVEXPOTXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Advexpotxt"},"ADVEXPONUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Advexponum"},"TIPOTASAADVEXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasaadvexpo"},"TASAIVAFRANJA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaivafranja"},"TASAIVAINTERIOR":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaivainterior"},"TASAISAN":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaisan"},"ARANCELMIXTO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Arancelmixto"},"TASAMIXTA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasamixta"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notas"},"HISTORICO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Historico"},"ARANCELESPECIFICO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Arancelespecifico"},"TIPOVEHICULO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipovehiculo"},"APLICAISAN":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Aplicaisan"},"APLICAIEPS":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Aplicaieps"},"NIVEL":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nivel"},"NICO":{"anyOf":[{"type":"string","maxLength":14},{"type":"null"}],"title":"Nico"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FraccionesResponse","description":"Schema for Fracciones response"},"FraccionesUSAResponse":{"properties":{"FRACCION_SIN_PUNTO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Sin Punto"},"FRACCION_CON_PUNTO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Con Punto"},"FRACCION_MOSTRAR":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Fraccion Mostrar"},"ESPECIFICO":{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}],"title":"Especifico"},"NIVEL":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Nivel"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Descripcion"},"UNIDADCANTIDAD":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Unidadcantidad"},"TARIFA1":{"anyOf":[{"type":"string","maxLength":250},{"type":"null"}],"title":"Tarifa1"},"TLC":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Tlc"},"TARIFA2":{"anyOf":[{"type":"string","maxLength":250},{"type":"null"}],"title":"Tarifa2"},"NOTAS":{"anyOf":[{"type":"string","maxLength":7998},{"type":"null"}],"title":"Notas"},"CONSECUTIVO":{"type":"integer","title":"Consecutivo"}},"type":"object","required":["CONSECUTIVO"],"title":"FraccionesUSAResponse","description":"Schema for FraccionesUSA response"},"FundamentosTLCResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"TASA1NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa1Num"},"TASA2NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa2Num"},"FUNDAMENTO1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento1"},"FUNDAMENTO2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento2"},"PERIODO":{"anyOf":[{"type":"string","maxLength":299},{"type":"null"}],"title":"Periodo"},"MODALIDAD":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Modalidad"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Notas"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"FundamentosTLCResponse","description":"Schema for FundamentosTLC response"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IEPSResponse":{"properties":{"FRACCION_SIN_PUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion Sin Punto"},"FRACCION_CON_PUNTO":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion Con Punto"},"FUNDAMENTO":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Fundamento"},"CONDICION":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Condicion"},"TASA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa"},"TASAESPECIFICO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasaespecifico"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"CONSECUTIVO":{"type":"integer","title":"Consecutivo"}},"type":"object","required":["CONSECUTIVO"],"title":"IEPSResponse","description":"Schema for IEPS response"},"InformacionGeneralResponse":{"properties":{"OBSERVACIONES":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Observaciones"},"NOTAOBSERVACIONES":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Notaobservaciones"},"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"TIPOOPERACION":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipooperacion"},"ORDEN":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Orden"},"LEYENDA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Leyenda"},"NOTA":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nota"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"InformacionGeneralResponse","description":"Schema for InformacionGeneral response"},"LoginRequest":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"LoginRequest","description":"Schema for login request"},"NomsResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion","default":""},"IMPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Importacion","default":""},"EXPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Exportacion","default":""},"PERMISO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Permiso","default":""},"ACUERDO":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Acuerdo","default":""},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion","default":""},"FUNDAMENTO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento","default":""},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof","default":""},"FORMATONOM":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Formatonom","default":""},"INSTRUCCIONES":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Instrucciones","default":""},"CRITERIO":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Criterio","default":""},"COMPLEMENTO":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Complemento","default":""},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"NomsResponse","description":"Schema for Noms response"},"PaginatedFraccionesResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/FraccionesResponse"},"type":"array","title":"Data"},"total":{"type":"integer","title":"Total","description":"Total de registros que coinciden con la búsqueda"},"page":{"type":"integer","title":"Page","description":"Página actual (base 1)"},"limit":{"type":"integer","title":"Limit","description":"Registros por página"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total de páginas"}},"type":"object","required":["data","total","page","limit","total_pages"],"title":"PaginatedFraccionesResponse","description":"Paginated response wrapper for Fracciones"},"PaginatedFraccionesUSAResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/FraccionesUSAResponse"},"type":"array","title":"Data"},"total":{"type":"integer","title":"Total","description":"Total de registros que coinciden con la búsqueda"},"page":{"type":"integer","title":"Page","description":"Página actual (base 1)"},"limit":{"type":"integer","title":"Limit","description":"Registros por página"},"total_pages":{"type":"integer","title":"Total Pages","description":"Total de páginas"}},"type":"object","required":["data","total","page","limit","total_pages"],"title":"PaginatedFraccionesUSAResponse","description":"Paginated response wrapper for FraccionesUSA"},"PreciosEstimados2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":499},{"type":"null"}],"title":"Descripcion"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"PRECIOESTIMADODLLS":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Precioestimadodlls"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"OBSERVACION1":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion1"},"OBSERVACION2":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion2"},"RESOLUCION":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Resolucion"},"REGLA":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Regla"},"WEB_DOCUMENTO_ID_RES":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id Res"},"WEB_DOCUMENTO_ID_REG":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id Reg"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"PreciosEstimados2Response","description":"Schema for PreciosEstimados2 response"},"ProsecResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion","default":""},"ARTICULO":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Articulo","default":""},"SECTOR":{"anyOf":[{"type":"string","maxLength":6},{"type":"null"}],"title":"Sector","default":""},"TASATXT":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Tasatxt","default":""},"TASANUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasanum","default":"0"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa","default":0},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof","default":""},"OBSERVACION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Observacion","default":""},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"ProsecResponse","description":"Schema for Prosec response"},"RCG2Response":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ANEXO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Anexo"},"SECTOR":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sector"},"REGIMEN":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Regimen"},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion"},"ADUANAS":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Aduanas"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"ARCHIVOCRITERIO":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Archivocriterio"},"TIPO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipo"},"IMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impo"},"EXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expo"},"WEB_DOCUMENTO_ID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RCG2Response","description":"Schema for RCG2 response"},"REITResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"ARTICULO":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Articulo"},"FUNDAMENTO":{"anyOf":[{"type":"string","maxLength":1500},{"type":"null"}],"title":"Fundamento"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Acuerdo"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"DOCUMENTO":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Documento"},"TEMPORALIDAD":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidad"},"TEMPORALIDADSERVICIO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidadservicio"},"TEMPORALIDADCERTIFICADA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Temporalidadcertificada"},"WEB_DOCUMENTO_ID":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Web Documento Id"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"REITResponse","description":"Schema for REIT response"},"RegulacionesResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"Fraccion"},"IMPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Importacion"},"EXPORTACION":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Exportacion"},"PERMISO":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"Permiso"},"ACUERDO":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Acuerdo"},"CONDICION":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condicion"},"FUNDAMENTO":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fundamento"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"FORMATONOM":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Formatonom"},"INSTRUCCIONES":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Instrucciones"},"CRITERIO":{"anyOf":[{"type":"string","maxLength":254},{"type":"null"}],"title":"Criterio"},"COMPLEMENTO":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Complemento"},"CRITERIOESPECIAL":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Criterioespecial"},"PAIS":{"anyOf":[{"type":"string","maxLength":3},{"type":"null"}],"title":"Pais"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RegulacionesResponse","description":"Schema for Regulaciones response"},"RequisitoPrevioResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"DESCRIPCION":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Descripcion"},"OBSERVACION":{"anyOf":[{"type":"string","maxLength":1999},{"type":"null"}],"title":"Observacion"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"VIGENCIA":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Vigencia"},"NUMEROACUERDO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroacuerdo"},"NUMEROCRITERIO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numerocriterio"},"NUMEROFORMATO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroformato"},"NUMEROINSTRUCCIONES":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Numeroinstrucciones"},"HISTORICO":{"anyOf":[{"type":"string","maxLength":2000},{"type":"null"}],"title":"Historico"},"NOTA":{"anyOf":[{"type":"string","maxLength":5000},{"type":"null"}],"title":"Nota"},"PERMISO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Permiso"},"IMPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Impo"},"EXPO":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expo"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"RequisitoPrevioResponse","description":"Schema for RequisitoPrevio response"},"TLCSResponse":{"properties":{"FRACCION":{"type":"string","maxLength":10,"title":"Fraccion"},"PAIS":{"type":"string","maxLength":3,"title":"Pais"},"ORDEN":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Orden"},"TASATXT":{"anyOf":[{"type":"string","maxLength":44},{"type":"null"}],"title":"Tasatxt"},"TIPOTASA":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tipotasa"},"TASA1NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa1Num"},"FACTOR1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Factor1"},"TASA2NUM":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tasa2Num"},"FACTOR2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Factor2"},"DOF":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Dof"},"NOTAS":{"anyOf":[{"type":"string","maxLength":999},{"type":"null"}],"title":"Notas"},"NICO":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Nico","default":""},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["FRACCION","PAIS","SYSID"],"title":"TLCSResponse","description":"Schema for TLCS response"},"Token":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type"}},"type":"object","required":["access_token","token_type"],"title":"Token","description":"Schema for token response"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VehiculosMarcasResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"MARCA":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Marca"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"VehiculosMarcasResponse","description":"Schema for VehiculosMarcas response"},"VehiculosModelosResponse":{"properties":{"FRACCION":{"anyOf":[{"type":"string","maxLength":8},{"type":"null"}],"title":"Fraccion"},"MARCA":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"Marca"},"MODELO":{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}],"title":"Modelo"},"TIPO":{"anyOf":[{"type":"string","maxLength":1},{"type":"null"}],"title":"Tipo"},"CLAVEUM":{"anyOf":[{"type":"string","maxLength":2},{"type":"null"}],"title":"Claveum"},"A20":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A20"},"A19":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A19"},"A18":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A18"},"A17":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A17"},"A16":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A16"},"A15":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A15"},"A14":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A14"},"A13":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A13"},"A12":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A12"},"A11":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A11"},"A10":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A10"},"A9":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A9"},"A8":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A8"},"A7":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A7"},"A6":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A6"},"A5":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A5"},"A4":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A4"},"A3":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A3"},"A2":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A2"},"A1":{"anyOf":[{"type":"string","maxLength":19},{"type":"null"}],"title":"A1"},"SYSID":{"type":"integer","title":"Sysid"}},"type":"object","required":["SYSID"],"title":"VehiculosModelosResponse","description":"Schema for VehiculosModelos response"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}} \ No newline at end of file From 1dbc75107f9f8a12590591d581654d587db60ae0 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Fri, 8 May 2026 17:03:29 -0500 Subject: [PATCH 6/7] feat: enhance user avatar handling and synchronization with Workspace - Updated sidebar component to merge user data from Keycloak and Workspace, including avatar URLs. - Refactored nav-user component to utilize new avatar resolution logic and display user information more effectively. - Introduced a new utility function to resolve user avatar URLs, prioritizing Workspace avatars. - Implemented backend changes to support synchronization of user profile data, including avatar URLs from Workspace. - Added database migration to include new fields for Workspace profile synchronization in user_tenants. - Created a new client for fetching user profiles from Workspace. - Updated dashboard components to reflect changes in user data structure and avatar handling. - Removed avatar upload functionality from the profile update process, relying on Workspace for avatar management. - Added tests for avatar resolution logic to ensure correct prioritization of avatar sources. --- ...orkspace_profile_fields_to_user_tenants.py | 56 ++++ backend/api/v1/modules/core/auth/dto.py | 1 + backend/api/v1/modules/core/auth/service.py | 228 ++++++++++++++- .../api/v1/modules/core/user_tenant/models.py | 13 + backend/api/v1/modules/core/users/routes.py | 21 +- backend/api/v1/modules/core/users/service.py | 61 +++- backend/core/config.py | 16 +- backend/core/security.py | 15 + backend/core/workspace_profile_client.py | 80 +++++ backend/core/workspace_profile_sync.py | 114 ++++++++ frontend/src/lib/auth.ts | 273 ++++++++++++++++-- .../lib/components/sidebar/app-sidebar.svelte | 41 ++- .../lib/components/sidebar/nav-user.svelte | 64 +++- frontend/src/lib/server/api.ts | 46 ++- frontend/src/lib/utils.avatar.test.ts | 24 ++ .../src/lib/utils.getBackendAssetUrl.test.ts | 10 +- frontend/src/lib/utils.ts | 129 ++++++++- frontend/src/routes/auth/sso/+page.server.ts | 7 +- frontend/src/routes/dashboard/+layout.svelte | 11 + .../routes/dashboard/account/+page.server.ts | 37 +-- .../src/routes/dashboard/account/+page.svelte | 73 +---- 21 files changed, 1170 insertions(+), 150 deletions(-) create mode 100644 backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py create mode 100644 backend/core/workspace_profile_client.py create mode 100644 backend/core/workspace_profile_sync.py create mode 100644 frontend/src/lib/utils.avatar.test.ts diff --git a/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py b/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py new file mode 100644 index 00000000..7608f783 --- /dev/null +++ b/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py @@ -0,0 +1,56 @@ +"""add workspace profile fields to user_tenants + +Revision ID: c3d4e5f6a7b +Revises: b2c3d4e5f6a7 +Create Date: 2026-05-08 00:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "c3d4e5f6a7b" +down_revision: Union[str, None] = "b2c3d4e5f6a7" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "user_tenants", + sa.Column( + "workspace_user_id", + sa.String(length=255), + nullable=True, + comment="User ID (sub) proveniente de Workspace", + ), + schema="core", + ) + op.add_column( + "user_tenants", + sa.Column( + "workspace_avatar_url", + sa.String(length=500), + nullable=True, + comment="Avatar URL sincronizado desde Workspace", + ), + schema="core", + ) + op.add_column( + "user_tenants", + sa.Column( + "workspace_profile_synced_at", + sa.DateTime(timezone=True), + nullable=True, + comment="Última sincronización de perfil con Workspace", + ), + schema="core", + ) + + +def downgrade() -> None: + op.drop_column("user_tenants", "workspace_profile_synced_at", schema="core") + op.drop_column("user_tenants", "workspace_avatar_url", schema="core") + op.drop_column("user_tenants", "workspace_user_id", schema="core") diff --git a/backend/api/v1/modules/core/auth/dto.py b/backend/api/v1/modules/core/auth/dto.py index 519bdd46..a9bad09d 100644 --- a/backend/api/v1/modules/core/auth/dto.py +++ b/backend/api/v1/modules/core/auth/dto.py @@ -63,6 +63,7 @@ class UserInfoResponseDTO(BaseModel): preferred_username: Optional[str] = None tenant_id: Optional[int] = None tenant_slug: Optional[str] = None + avatar_url: Optional[str] = None roles: list[str] = [] permissions: list[str] = [] diff --git a/backend/api/v1/modules/core/auth/service.py b/backend/api/v1/modules/core/auth/service.py index c8c013b2..b64f2eca 100644 --- a/backend/api/v1/modules/core/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -1,6 +1,7 @@ import logging import httpx -from typing import Any, Dict +from typing import Any, Dict, Optional +from jose import JWTError, jwt from core.config import settings from fastapi import HTTPException @@ -23,6 +24,96 @@ class AuthService: def __init__(self, db: Session): self.db = db + @staticmethod + def _clean_text(value: Any) -> Optional[str]: + if isinstance(value, str): + cleaned = value.strip() + if cleaned: + return cleaned + return None + + def _pick_text(self, *candidates: Any) -> Optional[str]: + for candidate in candidates: + value = self._clean_text(candidate) + if value: + return value + return None + + def _decode_kc_user_from_token(self, access_token: str) -> Dict[str, Any]: + try: + claims = jwt.get_unverified_claims(access_token) + return claims if isinstance(claims, dict) else {} + except JWTError: + return {} + except Exception: + return {} + + async def _get_kc_admin_user(self, keycloak_user_id: Optional[str]) -> Optional[Dict[str, Any]]: + """ + Fallback de datos de usuario consultando el Hub admin API. + Es opcional y no debe romper /me si falla. + """ + if not keycloak_user_id: + return None + if not settings.HUB_ADMIN_EMAIL or not settings.HUB_ADMIN_PASSWORD: + return None + + try: + async with httpx.AsyncClient(timeout=10.0) as client: + login_resp = await client.post( + f"{settings.HUB_URL}api/v1/auth/login", + json={ + "username": settings.HUB_ADMIN_EMAIL, + "password": settings.HUB_ADMIN_PASSWORD, + }, + ) + if login_resp.status_code != 200: + return None + + admin_token = login_resp.json().get("access_token") + if not admin_token: + return None + + user_resp = await client.get( + f"{settings.HUB_URL}api/v1/hub/admins/{keycloak_user_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + if user_resp.status_code == 200: + payload = user_resp.json() + return payload if isinstance(payload, dict) else None + except Exception as exc: + logger.debug("kc_admin_user_lookup_failed: %s", exc) + + return None + + def _extract_avatar_url(self, *sources: Any) -> Optional[str]: + for source in sources: + if not isinstance(source, dict): + continue + + direct = self._pick_text( + source.get("avatar_url"), + source.get("avatarUrl"), + source.get("picture"), + source.get("photo"), + ) + if direct: + return direct + + attrs = source.get("attributes") + if isinstance(attrs, dict): + attr_candidate = attrs.get("avatar_url") + if isinstance(attr_candidate, list) and attr_candidate: + value = self._clean_text(attr_candidate[0]) + if value: + return value + if isinstance(attr_candidate, str): + value = self._clean_text(attr_candidate) + if value: + return value + + return None + async def login( self, login_data: LoginRequestDTO, @@ -55,6 +146,38 @@ class AuthService: except Exception as exc: logger.warning("Lazy-link invite check failed (non-blocking): %s", exc) + # Sync de perfil/avatar desde Workspace usando el mismo bearer. + # No bloquea login si Workspace no responde. + access_token = data.get("access_token") + if access_token: + from core.workspace_profile_sync import sync_workspace_profile_for_user + from core.workspace_profile_client import WorkspaceProfileClient + + workspace_profile = None + try: + workspace_profile = await WorkspaceProfileClient().get_me(access_token) + except Exception as exc: + logger.warning( + "workspace_profile_sync_failed", + extra={ + "event": "workspace_profile_sync_failed", + "phase": "login", + "error": str(exc), + }, + ) + workspace_profile = None + + await sync_workspace_profile_for_user( + self.db, + access_token=access_token, + keycloak_user_id=(workspace_profile or {}).get("sub") + or data.get("sub") + or data.get("user_id"), + tenant_id=data.get("tenant_id"), + workspace_profile=workspace_profile, + force=True, + ) + # AUDIT LOG: Login Success try: from api.v1.modules.a76.audit_log.services.service import AuditService @@ -102,7 +225,37 @@ class AuthService: ) if response.status_code == 200: - return TokenResponseDTO(**response.json()) + data = response.json() + from core.workspace_profile_sync import sync_workspace_profile_for_user + from core.workspace_profile_client import WorkspaceProfileClient + + workspace_profile = None + try: + workspace_profile = await WorkspaceProfileClient().get_me( + data.get("access_token", "") + ) + except Exception as exc: + logger.warning( + "workspace_profile_sync_failed", + extra={ + "event": "workspace_profile_sync_failed", + "phase": "refresh", + "error": str(exc), + }, + ) + workspace_profile = None + + await sync_workspace_profile_for_user( + self.db, + access_token=data.get("access_token"), + keycloak_user_id=(workspace_profile or {}).get("sub") + or data.get("sub") + or data.get("user_id"), + tenant_id=data.get("tenant_id"), + workspace_profile=workspace_profile, + force=True, + ) + return TokenResponseDTO(**data) raise HTTPException(status_code=401, detail="Invalid or expired refresh token") @@ -115,9 +268,78 @@ class AuthService: Obtiene información del usuario desde el Hub """ from core.security import verify_token + from core.workspace_profile_sync import sync_workspace_profile_for_user # Aprovechamos la verificación (y cache) de security.py user_info = await verify_token(access_token) - return UserInfoResponseDTO(**user_info) + + kc_user = self._decode_kc_user_from_token(access_token) + keycloak_user_id = self._pick_text(user_info.get("sub"), kc_user.get("sub")) + + needs_admin_fallback = any( + not self._clean_text(user_info.get(field)) + for field in ("email", "preferred_username") + ) or self._extract_avatar_url(user_info) is None + + kc_admin_user = None + if needs_admin_fallback: + kc_admin_user = await self._get_kc_admin_user(keycloak_user_id) + + first_name = self._pick_text( + user_info.get("first_name"), + user_info.get("given_name"), + kc_user.get("given_name"), + kc_user.get("first_name"), + (kc_admin_user or {}).get("firstName"), + (kc_admin_user or {}).get("first_name"), + ) + last_name = self._pick_text( + user_info.get("last_name"), + user_info.get("family_name"), + kc_user.get("family_name"), + kc_user.get("last_name"), + (kc_admin_user or {}).get("lastName"), + (kc_admin_user or {}).get("last_name"), + ) + full_name = self._pick_text( + f"{first_name} {last_name}" if first_name and last_name else None, + first_name, + last_name, + ) + + enriched_user_info = dict(user_info) + enriched_user_info["sub"] = keycloak_user_id or user_info.get("sub") + enriched_user_info["email"] = self._pick_text( + user_info.get("email"), + (kc_admin_user or {}).get("email"), + kc_user.get("email"), + ) + enriched_user_info["preferred_username"] = self._pick_text( + user_info.get("preferred_username"), + user_info.get("username"), + kc_user.get("preferred_username"), + kc_user.get("username"), + (kc_admin_user or {}).get("username"), + ) + enriched_user_info["avatar_url"] = self._extract_avatar_url( + user_info, + kc_user, + kc_admin_user or {}, + ) + enriched_user_info["name"] = self._pick_text( + user_info.get("name"), + full_name, + kc_user.get("name"), + enriched_user_info.get("preferred_username"), + ) + + await sync_workspace_profile_for_user( + self.db, + access_token=access_token, + keycloak_user_id=enriched_user_info.get("sub"), + tenant_id=enriched_user_info.get("tenant_id"), + workspace_profile=enriched_user_info, + ) + return UserInfoResponseDTO(**enriched_user_info) async def logout(self, logout_data: LogoutRequestDTO) -> dict: """ diff --git a/backend/api/v1/modules/core/user_tenant/models.py b/backend/api/v1/modules/core/user_tenant/models.py index 33138d6e..d25c00be 100644 --- a/backend/api/v1/modules/core/user_tenant/models.py +++ b/backend/api/v1/modules/core/user_tenant/models.py @@ -2,6 +2,7 @@ Modelo de relación entre usuarios (Keycloak) y tenants """ +from datetime import datetime from typing import TYPE_CHECKING, Optional from api.v1.common.base_models import TenantScopedMixin, TimestampMixin @@ -13,6 +14,7 @@ from sqlalchemy import ( String, Text, UniqueConstraint, + DateTime, ) from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -56,6 +58,17 @@ class UserTenant(Base, TenantScopedMixin, TimestampMixin): avatar_url: Mapped[Optional[str]] = mapped_column( String(500), nullable=True, comment="URL de la imagen de perfil" ) + workspace_user_id: Mapped[Optional[str]] = mapped_column( + String(255), nullable=True, comment="User ID (sub) proveniente de Workspace" + ) + workspace_avatar_url: Mapped[Optional[str]] = mapped_column( + String(500), nullable=True, comment="Avatar URL sincronizado desde Workspace" + ) + workspace_profile_synced_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + comment="Última sincronización de perfil con Workspace", + ) # Caché local de nombre/apellido (fuente de verdad = Keycloak vía Hub; # se sincroniza al editar perfil desde Anexo76) first_name: Mapped[Optional[str]] = mapped_column( diff --git a/backend/api/v1/modules/core/users/routes.py b/backend/api/v1/modules/core/users/routes.py index b6dfbe7a..8856d43d 100644 --- a/backend/api/v1/modules/core/users/routes.py +++ b/backend/api/v1/modules/core/users/routes.py @@ -154,6 +154,7 @@ def get_user_avatar_image( @router.get("/me/profile", response_model=UserResponseDTO) async def get_my_profile( + request: Request, current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): @@ -180,7 +181,17 @@ async def get_my_profile( ) service = UserService(db, user_tenant.tenant_id, user_tenant.company_id) - return await service.get_current_user_profile(keycloak_user_id, current_user=current_user) + auth_header = request.headers.get("Authorization") or "" + access_token = ( + auth_header[7:].strip() + if auth_header.lower().startswith("bearer ") + else auth_header.strip() or None + ) + return await service.get_current_user_profile( + keycloak_user_id, + current_user=current_user, + access_token=access_token, + ) @router.put("/me/profile", response_model=UserResponseDTO) @@ -214,6 +225,14 @@ async def update_my_profile( status_code=400, detail="User does not belong to any tenant" ) + # Para sesiones autenticadas vía Workspace/Hub, la foto de perfil viene del Hub + # y no debe mutarse localmente en Anexo76. + if current_user.get("sub"): + raise HTTPException( + status_code=409, + detail="Avatar is managed by Workspace for this user", + ) + auth_header = request.headers.get("Authorization") or "" access_token = ( auth_header[7:].strip() diff --git a/backend/api/v1/modules/core/users/service.py b/backend/api/v1/modules/core/users/service.py index 961e4bfa..9d7f1b4b 100644 --- a/backend/api/v1/modules/core/users/service.py +++ b/backend/api/v1/modules/core/users/service.py @@ -2,6 +2,7 @@ import logging import httpx from datetime import datetime from typing import Any, Dict, List, Optional +from urllib.parse import urlparse from fastapi import HTTPException from sqlalchemy import and_, func @@ -15,6 +16,30 @@ from ..user_tenant.models import UserTenant logger = logging.getLogger(__name__) +def _is_valid_http_url(url: Optional[str]) -> bool: + if not url or not isinstance(url, str): + return False + parsed = urlparse(url.strip()) + return parsed.scheme in ("http", "https") and bool(parsed.netloc) + + +def _legacy_avatar_public_url(user_tenant: Optional[Any]) -> Optional[str]: + if not user_tenant or not user_tenant.avatar_url: + return None + avatar_out = str(user_tenant.avatar_url) + + if avatar_out.startswith("http://") or avatar_out.startswith("https://"): + return avatar_out if _is_valid_http_url(avatar_out) else None + + from core.s3_keys import public_user_avatar_api_path + + # Entregamos siempre el endpoint público del backend para assets locales/S3. + return public_user_avatar_api_path( + user_tenant.tenant_id, + user_tenant.keycloak_user_id, + ) + + def _normalize_user( user_data: Dict[str, Any], role: Optional[str] = None, @@ -43,18 +68,20 @@ def _normalize_user( # Agregar campos de perfil si user_tenant está disponible if user_tenant: - avatar_out = user_tenant.avatar_url - if avatar_out: - from core.s3_keys import public_user_avatar_api_path + workspace_avatar = ( + user_tenant.workspace_avatar_url + if _is_valid_http_url(user_tenant.workspace_avatar_url) + else None + ) + legacy_avatar = _legacy_avatar_public_url(user_tenant) + avatar_out = workspace_avatar or legacy_avatar - # Siempre devolver la URL pública del endpoint de servicio de imágenes, - # independientemente de si es clave S3 (tenants/...) o ruta local (/uploads/...). - avatar_out = public_user_avatar_api_path( - user_tenant.tenant_id, user_tenant.keycloak_user_id - ) normalized.update( { "avatar_url": avatar_out, + "workspace_avatar_url": workspace_avatar, + "legacy_avatar_url": legacy_avatar, + "workspace_user_id": user_tenant.workspace_user_id, "phone": user_tenant.phone, "bio": user_tenant.bio, "preferences": user_tenant.preferences or {}, @@ -629,11 +656,27 @@ class UserService: "usage_percentage": round(usage_percentage, 2), } - async def get_current_user_profile(self, keycloak_user_id: str, current_user: Dict[str, Any] = None) -> Dict[str, Any]: + async def get_current_user_profile( + self, + keycloak_user_id: str, + current_user: Dict[str, Any] = None, + access_token: Optional[str] = None, + ) -> Dict[str, Any]: """Obtiene el perfil completo del usuario actual""" # Use the already-verified JWT claims dict — do NOT call verify_token(uuid) user_info = current_user or {"id": keycloak_user_id} + # Perfil "me": sincronización con cache corto (5 min). + if access_token: + from core.workspace_profile_sync import sync_workspace_profile_for_user + + await sync_workspace_profile_for_user( + self.db, + access_token=access_token, + keycloak_user_id=keycloak_user_id, + tenant_id=self.tenant_id, + ) + user_tenant = self.db.query(UserTenant).filter( and_(UserTenant.keycloak_user_id == keycloak_user_id, UserTenant.is_active == True) ).first() diff --git a/backend/core/config.py b/backend/core/config.py index 68755842..752933e1 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -41,6 +41,9 @@ class Settings(BaseSettings): # Hub de Aduanasoft — requerido siempre (SaaS y self-hosted) HUB_URL: str = "http://localhost:8001" + # Base API del Hub/Workspace para endpoint /v1/auth/me (fuente de verdad de perfil) + HUB_API_BASE_URL: str = "" + HUB_PROFILE_SYNC_TIMEOUT_MS: int = 3000 # Cuenta de servicio Hub — usada para operaciones admin (ej. sync de nombre a Keycloak) HUB_ADMIN_EMAIL: str = "" HUB_ADMIN_PASSWORD: str = "" @@ -48,7 +51,7 @@ class Settings(BaseSettings): # URL pública del frontend — usada en links de email (invitaciones, etc.) APP_PUBLIC_URL: str = "http://localhost:3000" - @field_validator("CENTRAL_SERVER_URL", "SPOKE_URLS", "HUB_URL", mode="before") + @field_validator("CENTRAL_SERVER_URL", "SPOKE_URLS", "HUB_URL", "HUB_API_BASE_URL", mode="before") @classmethod def strip_quotes(cls, v: str) -> str: if v and isinstance(v, str): @@ -116,6 +119,17 @@ class Settings(BaseSettings): """ return self.CSV_IMPORT_STORAGE == "minio" or self.S3_FILE_STORAGE + @property + def hub_api_base_url(self) -> str: + """ + Base URL para endpoints /v1 del Workspace/Hub. + Si HUB_API_BASE_URL no está definido, deriva de HUB_URL + /api. + """ + custom = (self.HUB_API_BASE_URL or "").strip().rstrip("/") + if custom: + return custom + return f"{self.HUB_URL.rstrip('/')}/api" + # Instancia global de configuración settings = Settings() diff --git a/backend/core/security.py b/backend/core/security.py index 328ebdce..353eea9c 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -344,6 +344,21 @@ async def get_current_user( ) user_info["tenant_id"] = effective_tenant_id + # Rehidratación de sesión: sincronización no bloqueante de avatar/perfil + # con cache corto para evitar llamadas excesivas al Hub. + try: + from core.workspace_profile_sync import sync_workspace_profile_for_user + + await sync_workspace_profile_for_user( + db, + access_token=token, + keycloak_user_id=user_info.get("sub"), + tenant_id=user_info.get("tenant_id"), + workspace_profile=user_info, + ) + except Exception as exc: + logger.warning("workspace_profile_sync_failed_on_get_current_user: %s", exc) + return user_info diff --git a/backend/core/workspace_profile_client.py b/backend/core/workspace_profile_client.py new file mode 100644 index 00000000..bcc31d7e --- /dev/null +++ b/backend/core/workspace_profile_client.py @@ -0,0 +1,80 @@ +import asyncio +import logging +from typing import Any, Optional + +import httpx + +from core.config import settings + +logger = logging.getLogger(__name__) + + +class WorkspaceProfileClient: + """Cliente para consultar perfil del usuario en Workspace Hub (/v1/auth/me).""" + + def __init__( + self, + base_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + retries: int = 2, + transport: Optional[httpx.BaseTransport] = None, + ): + self.base_url = (base_url or settings.hub_api_base_url).rstrip("/") + self.timeout_s = max(0.1, float(timeout_ms or settings.HUB_PROFILE_SYNC_TIMEOUT_MS) / 1000.0) + self.retries = max(0, int(retries)) + self.transport = transport + + async def get_me(self, access_token: str) -> dict[str, Any]: + if not access_token: + raise ValueError("access_token is required") + + headers = {"Authorization": f"Bearer {access_token}"} + url = f"{self.base_url}/v1/auth/me" + + last_error: Optional[Exception] = None + for attempt in range(self.retries + 1): + try: + async with httpx.AsyncClient( + timeout=self.timeout_s, + transport=self.transport, + ) as client: + response = await client.get(url, headers=headers) + + if response.status_code == 200: + payload = response.json() + if not isinstance(payload, dict): + raise ValueError("Invalid workspace profile payload") + return payload + + if response.status_code in (401, 403, 404): + # Errores de autenticación/autorización o endpoint no disponible: + # no vale la pena reintentar. + raise httpx.HTTPStatusError( + f"Workspace profile request failed with status {response.status_code}", + request=response.request, + response=response, + ) + + # Reintentar solo para errores transitorios 5xx. + if response.status_code >= 500 and attempt < self.retries: + await asyncio.sleep(0.15 * (attempt + 1)) + continue + + raise httpx.HTTPStatusError( + f"Workspace profile request failed with status {response.status_code}", + request=response.request, + response=response, + ) + + except (httpx.TimeoutException, httpx.NetworkError) as exc: + last_error = exc + if attempt >= self.retries: + break + await asyncio.sleep(0.15 * (attempt + 1)) + except Exception as exc: + last_error = exc + break + + if last_error: + raise last_error + raise RuntimeError("Workspace profile request failed") diff --git a/backend/core/workspace_profile_sync.py b/backend/core/workspace_profile_sync.py new file mode 100644 index 00000000..9f34e899 --- /dev/null +++ b/backend/core/workspace_profile_sync.py @@ -0,0 +1,114 @@ +import logging +from datetime import datetime, timedelta, timezone +from typing import Any, Optional +from urllib.parse import urlparse + +from sqlalchemy.orm import Session + +from api.v1.modules.core.user_tenant.models import UserTenant +from core.workspace_profile_client import WorkspaceProfileClient + +logger = logging.getLogger(__name__) + +SYNC_TTL_SECONDS = 300 + + +def _is_valid_http_url(url: Optional[str]) -> bool: + if not url or not isinstance(url, str): + return False + parsed = urlparse(url.strip()) + return parsed.scheme in ("http", "https") and bool(parsed.netloc) + + +def _is_fresh(ts: Optional[datetime], ttl_seconds: int = SYNC_TTL_SECONDS) -> bool: + if not ts: + return False + now = datetime.now(timezone.utc) + if ts.tzinfo is None: + ts = ts.replace(tzinfo=timezone.utc) + return ts >= (now - timedelta(seconds=ttl_seconds)) + + +async def sync_workspace_profile_for_user( + db: Session, + *, + access_token: Optional[str], + keycloak_user_id: Optional[str], + tenant_id: Optional[int] = None, + company_id: Optional[int] = None, + workspace_profile: Optional[dict[str, Any]] = None, + force: bool = False, +) -> None: + """ + Sincroniza sub/avatar_url desde Workspace hacia core.user_tenants. + Nunca lanza excepción para no bloquear login ni requests autenticados. + """ + if not access_token or not keycloak_user_id: + return + + try: + query = db.query(UserTenant).filter( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.is_active == True, + ) + if tenant_id is not None: + query = query.filter(UserTenant.tenant_id == int(tenant_id)) + if company_id is not None: + query = query.filter(UserTenant.company_id == int(company_id)) + + target = query.first() + if not target: + return + + if not force and _is_fresh(target.workspace_profile_synced_at): + return + + payload = workspace_profile + if payload is None: + client = WorkspaceProfileClient() + payload = await client.get_me(access_token) + + workspace_sub = payload.get("sub") + if not workspace_sub: + logger.warning( + "workspace_profile_sync_warning", + extra={ + "event": "workspace_profile_sync_warning", + "reason": "missing_sub", + "keycloak_user_id": keycloak_user_id, + "tenant_id": target.tenant_id, + }, + ) + return + + avatar_url = payload.get("avatar_url") + sanitized_avatar = avatar_url.strip() if isinstance(avatar_url, str) else None + if sanitized_avatar and not _is_valid_http_url(sanitized_avatar): + logger.warning( + "workspace_profile_sync_warning", + extra={ + "event": "workspace_profile_sync_warning", + "reason": "invalid_avatar_url", + "keycloak_user_id": keycloak_user_id, + "tenant_id": target.tenant_id, + }, + ) + sanitized_avatar = None + + target.workspace_user_id = str(workspace_sub) + target.workspace_avatar_url = sanitized_avatar + target.workspace_profile_synced_at = datetime.now(timezone.utc) + db.add(target) + db.commit() + except Exception as exc: + db.rollback() + logger.warning( + "workspace_profile_sync_failed", + extra={ + "event": "workspace_profile_sync_failed", + "error": str(exc), + "keycloak_user_id": keycloak_user_id, + "tenant_id": tenant_id, + "company_id": company_id, + }, + ) diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 490d7c7b..c91b5047 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -9,7 +9,7 @@ */ import Keycloak from 'keycloak-js'; -import { writable, derived } from 'svelte/store'; +import { writable, derived, get } from 'svelte/store'; import { browser } from '$app/environment'; import { clearAccessTokenOnDocument, @@ -26,9 +26,17 @@ export interface User { username: string; email?: string; name?: string; + firstName?: string | null; + lastName?: string | null; + displayName?: string | null; + avatarUrl?: string | null; + workspaceAvatarUrl?: string | null; + legacyAvatarUrl?: string | null; tenantId?: number; roles: string[]; permissions: string[]; + // Cache management + profileSyncedAt?: number; // timestamp en ms para cache TTL } export interface AuthState { @@ -49,6 +57,51 @@ const keycloakConfig = { }; let keycloakInstance: Keycloak | null = null; +const AUTH_USER_SESSION_KEY = 'anexo76_auth_user_v1'; + +function pickAvatar(...candidates: Array): string | null { + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.trim().length > 0) { + return candidate.trim(); + } + } + return null; +} + +function pickText(...candidates: Array): string | null { + for (const candidate of candidates) { + if (typeof candidate === 'string') { + const value = candidate.trim(); + if (value.length > 0) { + return value; + } + } + } + return null; +} + +function readUserFromSession(): User | null { + if (!browser) return null; + try { + const raw = sessionStorage.getItem(AUTH_USER_SESSION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as User; + if (!parsed || typeof parsed !== 'object') return null; + if (!parsed.id || !parsed.username) return null; + return parsed; + } catch { + return null; + } +} + +function persistUserInSession(user: User | null): void { + if (!browser) return; + if (!user) { + sessionStorage.removeItem(AUTH_USER_SESSION_KEY); + return; + } + sessionStorage.setItem(AUTH_USER_SESSION_KEY, JSON.stringify(user)); +} // ───────────────────────────────────────────────────────── // Auth store (tokens solo en memoria) @@ -68,7 +121,10 @@ const createAuthStore = () => { update((s) => ({ ...s, isAuthenticated: authenticated })), setLoading: (loading: boolean) => update((s) => ({ ...s, isLoading: loading })), - setUser: (user: User | null) => update((s) => ({ ...s, user })), + setUser: (user: User | null) => { + persistUserInSession(user); + update((s) => ({ ...s, user })); + }, setToken: (token: string | null) => update((s) => ({ ...s, token })), /** ⚠️ Los tokens ya NO se guardan en localStorage; solo en memoria. */ setTokens: (accessToken: string, _refreshToken?: string) => { @@ -77,12 +133,15 @@ const createAuthStore = () => { // el cliente no lo almacena ni lo lee en ningún momento. }, reset: () => - set({ - isAuthenticated: false, - isLoading: false, - user: null, - token: null - }) + { + persistUserInSession(null); + set({ + isAuthenticated: false, + isLoading: false, + user: null, + token: null + }); + } }; }; @@ -114,6 +173,11 @@ export const initAuth = async (): Promise => { try { authStore.setLoading(true); + const sessionUser = readUserFromSession(); + if (sessionUser) { + authStore.setUser(sessionUser); + } + // Restaurar token desde cookie no-HttpOnly (password login flow) const cookieToken = getAccessTokenFromDocument(); if (cookieToken) { @@ -166,6 +230,7 @@ let previousTenantId: number | undefined = undefined; const updateAuthState = async () => { if (!keycloakInstance?.authenticated) { authStore.reset(); + persistUserInSession(null); return; } @@ -189,14 +254,43 @@ const updateAuthState = async () => { currentPerms = currentState.user?.permissions || []; } catch { } + const previousUser = get(authStore).user; + const firstName = pickText(profile.firstName, parsed?.given_name, previousUser?.firstName); + const lastName = pickText(profile.lastName, parsed?.family_name, previousUser?.lastName); + const fullNameFromParts = pickText( + firstName && lastName ? `${firstName} ${lastName}` : null, + firstName, + lastName + ); + const username = pickText( + profile.username, + parsed?.preferred_username, + parsed?.username, + previousUser?.username + ) ?? ''; + const name = pickText( + profile.firstName || profile.lastName ? `${profile.firstName ?? ''} ${profile.lastName ?? ''}` : null, + fullNameFromParts, + parsed?.name, + previousUser?.name, + username + ) ?? username; + const user: User = { - id: profile.id ?? '', - username: profile.username ?? '', - email: profile.email, - name: `${profile.firstName ?? ''} ${profile.lastName ?? ''}`.trim(), + id: pickText(profile.id, parsed?.sub, previousUser?.id) ?? '', + username, + email: pickText(profile.email, parsed?.email, previousUser?.email) ?? undefined, + name, + firstName, + lastName, + displayName: pickText(name, previousUser?.displayName, username), + avatarUrl: pickAvatar(previousUser?.avatarUrl), + workspaceAvatarUrl: pickAvatar(previousUser?.workspaceAvatarUrl), + legacyAvatarUrl: pickAvatar(previousUser?.legacyAvatarUrl), tenantId, roles, - permissions: parsed?.permissions?.length ? parsed.permissions : currentPerms + permissions: parsed?.permissions?.length ? parsed.permissions : currentPerms, + profileSyncedAt: previousUser?.profileSyncedAt }; authStore.setAuthenticated(true); @@ -338,15 +432,74 @@ const loadUserInfo = async (token: string) => { const { api } = await import('./api'); const response = await api.auth.me(); if (response.data) { + const previousUser = get(authStore).user; const d = response.data; + const workspaceAvatarUrl = pickAvatar( + d.workspaceAvatarUrl, + d.workspace_avatar_url, + d.avatar_url, + d.avatarUrl, + d.picture, + d.photo, + previousUser?.workspaceAvatarUrl + ); + const legacyAvatarUrl = pickAvatar( + d.legacyAvatarUrl, + d.legacy_avatar_url, + d.avatar, + d.photo, + d.picture, + previousUser?.legacyAvatarUrl + ); + const avatarUrl = pickAvatar(workspaceAvatarUrl, legacyAvatarUrl, previousUser?.avatarUrl); + + // Merge no destructivo: nunca pisar datos válidos con campos vacíos + const firstName = pickText(d.first_name, d.firstName, previousUser?.firstName); + const lastName = pickText(d.last_name, d.lastName, previousUser?.lastName); + const username = pickText( + d.preferred_username, + d.username, + previousUser?.username + ) ?? ''; + const nameFromParts = pickText( + firstName && lastName ? `${firstName} ${lastName}` : null, + firstName, + lastName + ); + const name = pickText( + d.name, + nameFromParts, + previousUser?.name, + username + ) ?? username; + const displayName = pickText( + d.displayName, + d.display_name, + name, + username + ) ?? username; + const email = pickText(d.email, previousUser?.email) ?? undefined; + const userId = pickText(d.sub, d.id, previousUser?.id) ?? ''; + + console.debug('[avatar][auth.loadUserInfo] /v1/auth/me avatar_url recibido:', workspaceAvatarUrl ?? '(null)'); + console.debug('[avatar][auth.loadUserInfo] avatar final para authStore:', avatarUrl ?? '(null)'); + console.debug('[profile][auth.loadUserInfo] first_name:', firstName ?? '(null)', 'last_name:', lastName ?? '(null)', 'avatar_url:', workspaceAvatarUrl ?? '(null)'); + authStore.setUser({ - id: d.sub ?? '', - username: d.preferred_username ?? d.username ?? '', - email: d.email, - name: d.name, - tenantId: d.tenant_id, - roles: d.roles ?? [], - permissions: d.permissions ?? [] + id: userId, + username, + email, + name, + firstName, + lastName, + displayName, + avatarUrl, + workspaceAvatarUrl, + legacyAvatarUrl, + tenantId: d.tenant_id ?? previousUser?.tenantId, + roles: d.roles ?? previousUser?.roles ?? [], + permissions: d.permissions ?? previousUser?.permissions ?? [], + profileSyncedAt: Date.now() }); } } catch (err) { @@ -354,6 +507,85 @@ const loadUserInfo = async (token: string) => { } }; +/** + * Sincroniza el perfil del usuario desde /v1/auth/me + * - Valida cache TTL (5 minutos) antes de hacer fetch + * - Extrae first_name, last_name, avatar_url + * - Retorna objeto con campos de perfil para UI o update de store + * + * Uso: + * ``` + * const profile = await syncUserProfile(accessToken); + * if (profile) { + * // profile.firstName, profile.lastName, profile.displayName, profile.avatarUrl + * } + * ``` + */ +export const syncUserProfile = async (accessToken?: string): Promise<{ + firstName: string | null; + lastName: string | null; + displayName: string | null; + avatarUrl: string | null; + workspaceAvatarUrl: string | null; + legacyAvatarUrl: string | null; + rawProfileSyncedAt: number; +} | null> => { + if (!browser) return null; + + try { + // Validar cache TTL: 5 minutos (300000ms) + const PROFILE_CACHE_TTL = 5 * 60 * 1000; + const { get } = await import('svelte/store'); + const currentState = get(authStore); + const now = Date.now(); + + if ( + currentState.user?.profileSyncedAt && + (now - currentState.user.profileSyncedAt) < PROFILE_CACHE_TTL + ) { + console.debug('[profile][sync] Cache válido, no re-fetching /v1/auth/me'); + return { + firstName: currentState.user.firstName ?? null, + lastName: currentState.user.lastName ?? null, + displayName: currentState.user.displayName ?? null, + avatarUrl: currentState.user.avatarUrl ?? null, + workspaceAvatarUrl: currentState.user.workspaceAvatarUrl ?? null, + legacyAvatarUrl: currentState.user.legacyAvatarUrl ?? null, + rawProfileSyncedAt: currentState.user.profileSyncedAt + }; + } + + const token = accessToken || getToken(); + if (!token) { + console.warn('[profile][sync] No token disponible para sincronizar'); + return null; + } + + // Llamar a loadUserInfo que hace fetch a /v1/auth/me + await loadUserInfo(token); + + // Retornar los nuevos valores desde el store + const updatedState = get(authStore); + if (updatedState.user) { + console.debug('[profile][sync] Perfil sincronizado exitosamente'); + return { + firstName: updatedState.user.firstName ?? null, + lastName: updatedState.user.lastName ?? null, + displayName: updatedState.user.displayName ?? null, + avatarUrl: updatedState.user.avatarUrl ?? null, + workspaceAvatarUrl: updatedState.user.workspaceAvatarUrl ?? null, + legacyAvatarUrl: updatedState.user.legacyAvatarUrl ?? null, + rawProfileSyncedAt: updatedState.user.profileSyncedAt ?? 0 + }; + } + + return null; + } catch (err) { + console.error('[profile][sync] Error sincronizando perfil:', err); + return null; + } +}; + // ───────────────────────────────────────────────────────── // Logout // ───────────────────────────────────────────────────────── @@ -376,6 +608,7 @@ export const logout = async () => { // Limpiar estado en memoria authStore.reset(); + persistUserInSession(null); clearAccessTokenOnDocument(); // La cookie HttpOnly del refresh_token la limpia el servidor diff --git a/frontend/src/lib/components/sidebar/app-sidebar.svelte b/frontend/src/lib/components/sidebar/app-sidebar.svelte index b9774567..92d92fd3 100644 --- a/frontend/src/lib/components/sidebar/app-sidebar.svelte +++ b/frontend/src/lib/components/sidebar/app-sidebar.svelte @@ -3,6 +3,7 @@ import { page } from "$app/state"; import { useSidebar } from "$lib/components/ui/sidebar/context.svelte.js"; import { getSidebarData } from "$lib/components/sidebar/modules"; + import { currentUser } from "$lib/auth"; import NavMain from "./nav-main.svelte"; import NavProjects from "./nav-projects.svelte"; import NavUser from "./nav-user.svelte"; @@ -21,19 +22,51 @@ // Obtener datos del sidebar con traducciones const sidebarData = getSidebarData(); + const mergedUser = $derived((page.data.user as any) || $currentUser || null); // Combinar los datos estáticos del sidebar con los datos del usuario de Keycloak const data = $derived({ ...sidebarData, - user: page.data.user + user: mergedUser ? { - name: _displayName(page.data.user), - email: page.data.user.email || "", - avatar: page.data.user.avatar_url || "/avatars/default.jpg", + name: _displayName(mergedUser), + email: mergedUser.email || "", + username: mergedUser.preferred_username || mergedUser.username || "", + firstName: mergedUser.first_name || mergedUser.firstName || mergedUser.given_name || null, + lastName: mergedUser.last_name || mergedUser.lastName || mergedUser.family_name || null, + displayName: + mergedUser.displayName || + _displayName(mergedUser) || + mergedUser.preferred_username || + mergedUser.username || + "", + avatarUrl: + mergedUser.workspaceAvatarUrl || + mergedUser.workspace_avatar_url || + mergedUser.avatarUrl || + mergedUser.avatar_url || + mergedUser.legacyAvatarUrl || + mergedUser.legacy_avatar_url || + null, + workspaceAvatarUrl: + mergedUser.workspaceAvatarUrl || + mergedUser.workspace_avatar_url || + null, + legacyAvatarUrl: + mergedUser.legacyAvatarUrl || + mergedUser.legacy_avatar_url || + mergedUser.avatar_url || + null, } : sidebarData.user, }); + $effect(() => { + if (mergedUser) { + console.debug('[avatar][sidebar] avatar final en page.data.user:', data.user?.avatarUrl ?? '(null)'); + } + }); + function _displayName(u: any): string { const first = u.first_name || u.given_name || ""; const last = u.last_name || u.family_name || ""; diff --git a/frontend/src/lib/components/sidebar/nav-user.svelte b/frontend/src/lib/components/sidebar/nav-user.svelte index ece25af4..bd73c6b3 100644 --- a/frontend/src/lib/components/sidebar/nav-user.svelte +++ b/frontend/src/lib/components/sidebar/nav-user.svelte @@ -19,11 +19,21 @@ import { page } from '$app/state'; import { goto } from '$app/navigation'; import { browser } from '$app/environment'; - import { getBackendAssetUrl } from '$lib/utils'; + import { resolveUserAvatarUrl } from '$lib/utils'; import AppVersion from '$lib/components/app-version.svelte'; let { user, tenants = [] }: { - user: { name: string; email: string; avatar: string }; + user: { + name: string; + email: string; + username?: string; + firstName?: string | null; + lastName?: string | null; + displayName?: string | null; + avatarUrl?: string | null; + workspaceAvatarUrl?: string | null; + legacyAvatarUrl?: string | null; + }; tenants: { id: number; name: string; slug: string }[]; } = $props(); const sidebar = useSidebar(); @@ -31,14 +41,40 @@ // Tenant activo (viene en el JWT como atributo tenant_slug) let currentTenantSlug = $derived((page.data.user as any)?.tenant_slug ?? ''); - // Estado de cambio de tenant + // State for tenant switching let switchingTenant = $state(false); - // URL completa del avatar - let avatarUrl = $derived(getBackendAssetUrl(user.avatar) || '/avatars/default.jpg'); + let avatarLoadFailed = $state(false); - // Iniciales del usuario (2 primeras letras) - let initials = $derived(user.name.slice(0, 2).toUpperCase()); + // URL de avatar con prioridad: Workspace -> legado + let avatarUrl = $derived( + avatarLoadFailed + ? '' + : resolveUserAvatarUrl( + user.workspaceAvatarUrl ?? null, + user.legacyAvatarUrl ?? user.avatarUrl ?? null + ) + ); + + $effect(() => { + console.debug('[avatar][nav-user] URL final usada por Avatar.Image:', avatarUrl || '(fallback)'); + }); + + // Nombre a mostrar: displayName > firstName + lastName > name > username + let displayName = $derived( + user.displayName || + (user.firstName && user.lastName ? `${user.firstName} ${user.lastName}`.trim() : null) || + user.name || + user.username || + 'User' + ); + + // Iniciales del usuario (2 primeras letras de displayName) + let initials = $derived(displayName.slice(0, 2).toUpperCase()); + + function handleAvatarError() { + avatarLoadFailed = true; + } // Estado reactivo del idioma actual let currentLocale = $derived(page.data.locale || 'en'); @@ -142,11 +178,11 @@ {...props} > - + {initials}
- {user.name} + {displayName} {user.email}
@@ -162,11 +198,11 @@
- - {initials} - -
- {user.name} + + {initials} + +
+ {displayName} {user.email}
diff --git a/frontend/src/lib/server/api.ts b/frontend/src/lib/server/api.ts index 190f44ca..3cd200c9 100644 --- a/frontend/src/lib/server/api.ts +++ b/frontend/src/lib/server/api.ts @@ -255,6 +255,15 @@ export async function validateAuth( fetch: typeof globalThis.fetch, redirectOnFail?: string ): Promise { + const pickAvatar = (...candidates: Array): string | null => { + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.trim().length > 0) { + return candidate.trim(); + } + } + return null; + }; + try { const response = await authenticatedFetch( 'v1/auth/me', @@ -273,6 +282,13 @@ export async function validateAuth( } const keycloakData = await response.json(); + const workspaceAvatarFromAuthMe = pickAvatar( + keycloakData.avatar_url, + keycloakData.avatarUrl, + keycloakData.picture, + keycloakData.photo + ); + console.debug('[avatar][validateAuth] /v1/auth/me avatar_url recibido:', workspaceAvatarFromAuthMe ?? '(null)'); // Obtener perfil adicional del usuario (avatar, bio, etc.) try { @@ -285,6 +301,23 @@ export async function validateAuth( if (profileResponse.ok) { const profileData = await profileResponse.json(); + const workspaceAvatarFromProfile = pickAvatar( + profileData.workspaceAvatarUrl, + profileData.workspace_avatar_url + ); + const legacyAvatar = pickAvatar( + profileData.legacyAvatarUrl, + profileData.legacy_avatar_url, + profileData.avatarUrl, + profileData.avatar_url, + profileData.avatar, + profileData.photo, + profileData.picture + ); + const finalWorkspaceAvatar = pickAvatar(workspaceAvatarFromAuthMe, workspaceAvatarFromProfile); + const finalAvatar = pickAvatar(finalWorkspaceAvatar, legacyAvatar); + console.debug('[avatar][validateAuth] avatar final resuelto:', finalAvatar ?? '(null)'); + // Combinar datos de Keycloak con datos del perfil. // Prioridad para nombre: caché local del perfil > JWT claims. return { @@ -294,7 +327,12 @@ export async function validateAuth( email: profileData.email || keycloakData.email || '', first_name: profileData.first_name || keycloakData.first_name || keycloakData.given_name || '', last_name: profileData.last_name || keycloakData.last_name || keycloakData.family_name || '', - avatar_url: profileData.avatar_url || null, + avatar_url: finalAvatar, + avatarUrl: finalAvatar, + workspace_avatar_url: finalWorkspaceAvatar, + workspaceAvatarUrl: finalWorkspaceAvatar, + legacy_avatar_url: legacyAvatar, + legacyAvatarUrl: legacyAvatar, phone: profileData.phone || null, bio: profileData.bio || null, preferences: profileData.preferences || {} @@ -306,12 +344,18 @@ export async function validateAuth( // Fallback: map raw JWT claim names to the expected field names const nameParts = (keycloakData.name || '').split(' '); + const finalAvatar = workspaceAvatarFromAuthMe; + console.debug('[avatar][validateAuth] fallback auth/me avatar final:', finalAvatar ?? '(null)'); return { ...keycloakData, id: keycloakData.id || keycloakData.sub, username: keycloakData.username || keycloakData.preferred_username || '', first_name: keycloakData.first_name || keycloakData.given_name || nameParts[0] || '', last_name: keycloakData.last_name || keycloakData.family_name || nameParts.slice(1).join(' ') || '', + avatar_url: finalAvatar, + avatarUrl: finalAvatar, + workspace_avatar_url: finalAvatar, + workspaceAvatarUrl: finalAvatar, }; } catch (error) { // Si es un redirect, re-lanzarlo diff --git a/frontend/src/lib/utils.avatar.test.ts b/frontend/src/lib/utils.avatar.test.ts new file mode 100644 index 00000000..dbcd7aea --- /dev/null +++ b/frontend/src/lib/utils.avatar.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { resolveUserAvatarUrl } from './utils'; + +describe('resolveUserAvatarUrl', () => { + it('prioriza avatar de Workspace cuando es URL absoluta', () => { + const value = resolveUserAvatarUrl('https://hub.example.com/media/avatar.png', '/uploads/legacy.png'); + expect(value).toBe('https://hub.example.com/media/avatar.png'); + }); + + it('acepta avatar de Workspace relativo cuando no hay VITE_HUB_URL', () => { + const value = resolveUserAvatarUrl('/media/avatar.png', '/uploads/legacy.png'); + expect(value).toBe('/media/avatar.png'); + }); + + it('usa avatar legado cuando Workspace no existe', () => { + const value = resolveUserAvatarUrl(null, '/uploads/legacy.png'); + expect(value).toBe('http://localhost:8000/uploads/legacy.png'); + }); + + it('retorna vacio para fallback visual cuando no hay ninguna imagen', () => { + const value = resolveUserAvatarUrl(null, null); + expect(value).toBe(''); + }); +}); diff --git a/frontend/src/lib/utils.getBackendAssetUrl.test.ts b/frontend/src/lib/utils.getBackendAssetUrl.test.ts index 140a91ed..47121ea9 100644 --- a/frontend/src/lib/utils.getBackendAssetUrl.test.ts +++ b/frontend/src/lib/utils.getBackendAssetUrl.test.ts @@ -15,8 +15,16 @@ describe('getBackendAssetUrl', () => { expect(getBackendAssetUrl('http://ejemplo.com/archivo.png')).toBe('http://ejemplo.com/archivo.png') }) + it('reescribe host interno de Docker a host publico', () => { + expect(getBackendAssetUrl('http://hub-backend:8000/api/static/avatars/file.png')).toBe('http://localhost:8000/api/static/avatars/file.png') + }) + + it('reescribe URL sin protocolo con host interno', () => { + expect(getBackendAssetUrl('hub-backend:8000/api/static/avatars/file.png')).toBe('http://localhost:8000/api/static/avatars/file.png') + }) + it('evita duplicar /api en la URL', () => { - expect(getBackendAssetUrl('/api/v1/items', 'http://localhost:8000/api')).toBe('http://localhost:8000/api/v1/items') + expect(getBackendAssetUrl('/api/v1/items')).toBe('http://localhost:8000/api/v1/items') }) it('construye URL completa para ruta relativa (VITE_API_URL por defecto host:8000)', () => { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index b57dde05..fca43238 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -5,6 +5,70 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }; +function isInternalDockerHost(hostname: string): boolean { + const host = hostname.toLowerCase(); + if (!host) return false; + if (host === 'localhost' || host === '127.0.0.1') return false; + if (host === 'backend' || host === 'hub-backend' || host === 'host.docker.internal') return true; + // Nombres de servicio Docker suelen no contener punto. + return !host.includes('.'); +} + +function getApiPublicBaseOrigin(): string { + const raw = (import.meta.env.VITE_API_URL || '').trim(); + if (raw) { + try { + const parsed = new URL(raw); + if (isInternalDockerHost(parsed.hostname)) { + if (typeof window !== 'undefined' && window.location?.origin) { + return window.location.origin; + } + return 'http://localhost:8000'; + } + return `${parsed.protocol}//${parsed.host}`; + } catch { + return raw.replace(/\/+$/, ''); + } + } + + if (typeof window !== 'undefined' && window.location?.origin) { + return window.location.origin; + } + + return 'http://localhost:8000'; +} + +function parseAbsoluteLikeUrl(value: string): URL | null { + const trimmed = value.trim(); + if (!trimmed) return null; + + try { + return new URL(trimmed); + } catch { + // Soporta formato host:puerto/ruta sin protocolo. + if (/^[a-z0-9.-]+:\d+\//i.test(trimmed)) { + try { + return new URL(`http://${trimmed}`); + } catch { + return null; + } + } + return null; + } +} + +function rewriteInternalHostToPublicUrl(value: string): string { + const parsed = parseAbsoluteLikeUrl(value); + if (!parsed) return value; + + if (isInternalDockerHost(parsed.hostname)) { + const publicOrigin = getApiPublicBaseOrigin(); + return `${publicOrigin}${parsed.pathname}${parsed.search}${parsed.hash}`; + } + + return value; +} + /** * Convierte una ruta relativa del backend en una URL completa * @param path Ruta relativa (ej: "/uploads/avatars/file.png") o absoluta API (ej: "/api/v1/...") @@ -15,7 +79,11 @@ export function getBackendAssetUrl(path: string | null | undefined): string { // Si ya es una URL completa, retornarla tal cual if (path.startsWith('http://') || path.startsWith('https://')) { - return path; + return rewriteInternalHostToPublicUrl(path); + } + + if (/^[a-z0-9.-]+:\d+\//i.test(path)) { + return rewriteInternalHostToPublicUrl(path); } const normalized = path.startsWith('/') ? path : `/${path}`; @@ -35,6 +103,65 @@ export function getBackendAssetUrl(path: string | null | undefined): string { return `${baseUrl}/${cleanPath}`; } +export function isSafeHttpUrl(value: string | null | undefined): boolean { + if (!value) return false; + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} + +function getHubAssetBaseUrl(): string { + const hubBase = (import.meta.env.VITE_HUB_URL || '').trim(); + if (!hubBase) return ''; + return hubBase.replace(/\/+$/, ''); +} + +function normalizeWorkspaceAvatarUrl(value: string | null | undefined): string { + if (!value || typeof value !== 'string') return ''; + const trimmed = value.trim(); + if (!trimmed) return ''; + + if (isSafeHttpUrl(trimmed)) { + return rewriteInternalHostToPublicUrl(trimmed); + } + + if (/^[a-z0-9.-]+:\d+\//i.test(trimmed)) { + return rewriteInternalHostToPublicUrl(trimmed); + } + + // Workspace puede devolver rutas relativas (ej: /media/avatar.png). + if (trimmed.startsWith('/')) { + const hubBase = getHubAssetBaseUrl(); + if (hubBase) { + return `${hubBase}${trimmed}`; + } + // Si no hay HUB_URL pública, intentar resolver en el mismo origen. + return trimmed; + } + + return ''; +} + +/** + * Prioridad de avatar de usuario: + * 1) workspaceAvatarUrl (http/https válido) + * 2) avatar local legado + * 3) fallback visual del componente Avatar + */ +export function resolveUserAvatarUrl( + workspaceAvatarUrl: string | null | undefined, + legacyAvatarUrl: string | null | undefined +): string { + const normalizedWorkspaceAvatar = normalizeWorkspaceAvatarUrl(workspaceAvatarUrl); + if (normalizedWorkspaceAvatar) { + return normalizedWorkspaceAvatar; + } + return getBackendAssetUrl(legacyAvatarUrl); +} + /** * Obtiene únicamente el nombre del archivo a partir de una ruta/URL. */ diff --git a/frontend/src/routes/auth/sso/+page.server.ts b/frontend/src/routes/auth/sso/+page.server.ts index a7978f7b..4c045a9a 100644 --- a/frontend/src/routes/auth/sso/+page.server.ts +++ b/frontend/src/routes/auth/sso/+page.server.ts @@ -32,10 +32,11 @@ export const load: PageServerLoad = async ({ url, cookies }) => { } // SSO exchange must call the Hub that GENERATED the relay token. - // HUB_URL is the canonical public Hub (workspace.aduanasoft.com) — where the - // App Launcher runs and where relay tokens are stored. - // INTERNAL_HUB_URL is a local mirror only used for token validation in the backend. + // This fetch runs server-side (inside the Docker container), so we must use + // INTERNAL_HUB_URL (host.docker.internal) when available — "localhost" inside + // a container never reaches the host where the workspace Hub is running. const hubUrl = ( + process.env.INTERNAL_HUB_URL || process.env.HUB_URL || process.env.VITE_HUB_URL || 'http://localhost:8001' diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 4228e45f..24aa41fd 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -78,11 +78,22 @@ } if (data.user) { + const resolvedAvatarUrl = + data.user.workspaceAvatarUrl || + data.user.workspace_avatar_url || + data.user.avatarUrl || + data.user.avatar_url || + data.user.legacyAvatarUrl || + data.user.legacy_avatar_url || + null; authStore.setUser({ id: data.user.sub ?? data.user.id ?? '', username: data.user.preferred_username ?? data.user.username ?? '', email: data.user.email, name: data.user.name, + avatarUrl: resolvedAvatarUrl, + workspaceAvatarUrl: data.user.workspaceAvatarUrl ?? data.user.workspace_avatar_url ?? null, + legacyAvatarUrl: data.user.legacyAvatarUrl ?? data.user.legacy_avatar_url ?? data.user.avatar_url ?? null, tenantId: data.user.tenant_id, roles: data.user.roles ?? [], permissions: data.user.permissions ?? [] diff --git a/frontend/src/routes/dashboard/account/+page.server.ts b/frontend/src/routes/dashboard/account/+page.server.ts index 1be0be04..4b0d6c3d 100644 --- a/frontend/src/routes/dashboard/account/+page.server.ts +++ b/frontend/src/routes/dashboard/account/+page.server.ts @@ -17,46 +17,13 @@ export const load: PageServerLoad = async ({ parent }) => { export const actions: Actions = { updateProfile: async ({ request, cookies, fetch }) => { - const formData = await request.formData(); - - // Manejar subida de avatar si existe - const avatarFile = formData.get('avatar') as File | null; - let avatarUrl: string | null = null; - - - if (avatarFile && avatarFile instanceof File && avatarFile.size > 0) { - try { - const uploadFormData = new FormData(); - uploadFormData.append('file', avatarFile); - - const uploadResponse = await authenticatedFetch( - 'v1/core/users/me/avatar', - { - method: 'POST', - body: uploadFormData - }, - cookies, - fetch, - '/login' - ); - - if (uploadResponse.ok) { - const result = await uploadResponse.json(); - avatarUrl = result.avatar_url; - } else { - const errorText = await uploadResponse.text(); - console.error('Upload failed:', errorText); - } - } catch (err) { - console.error('Error uploading avatar:', err); - } - } + const formData = await request.formData(); // Construir objeto de actualización desde FormData const updateData: Record = {}; for (const [key, value] of formData.entries()) { - if (key === 'avatar') continue; // Skip avatar file + if (key === 'avatar') continue; if (value && value !== '') { updateData[key] = value; } diff --git a/frontend/src/routes/dashboard/account/+page.svelte b/frontend/src/routes/dashboard/account/+page.svelte index 80c6cd8e..f86b6913 100644 --- a/frontend/src/routes/dashboard/account/+page.svelte +++ b/frontend/src/routes/dashboard/account/+page.svelte @@ -5,7 +5,7 @@ import { Label } from '$lib/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '$lib/components/ui/avatar'; - import { getBackendAssetUrl } from '$lib/utils'; + import { resolveUserAvatarUrl } from '$lib/utils'; import type { PageData, ActionData } from './$types'; let { data, form }: { data: PageData; form: ActionData } = $props(); @@ -18,8 +18,7 @@ let saving = $state(false); let success = $state(''); let error = $state(''); - let avatarFile = $state(null); - let avatarPreview = $state(''); + let avatarLoadFailed = $state(false); // Effect para manejar errores del servidor $effect(() => { @@ -32,8 +31,6 @@ $effect(() => { if (form?.success) { success = 'Perfil actualizado exitosamente'; - avatarPreview = ''; - avatarFile = null; setTimeout(() => { success = ''; }, 3000); @@ -44,34 +41,6 @@ } }); - async function handleAvatarChange(event: Event) { - const target = event.target as HTMLInputElement; - const file = target.files?.[0]; - - if (file) { - // Validate file type - if (!file.type.startsWith('image/')) { - error = 'Por favor selecciona una imagen válida'; - return; - } - - // Validate file size (max 2MB) - if (file.size > 2 * 1024 * 1024) { - error = 'La imagen debe ser menor a 2MB'; - return; - } - - avatarFile = file; - - // Create preview - const reader = new FileReader(); - reader.onload = (e) => { - avatarPreview = e.target?.result as string; - }; - reader.readAsDataURL(file); - } - } - function getInitials(profile: typeof data.profile): string { if (!profile) return '??'; const first = profile.first_name?.[0] || ''; @@ -79,8 +48,12 @@ return (first + last).toUpperCase() || profile.username?.[0]?.toUpperCase() || '?'; } + function handleAvatarError() { + avatarLoadFailed = true; + } + let currentAvatarUrl = $derived( - avatarPreview || getBackendAssetUrl(profile?.avatar_url) || '' + avatarLoadFailed ? '' : resolveUserAvatarUrl(profile?.workspace_avatar_url, profile?.legacy_avatar_url || profile?.avatar_url) ); @@ -109,11 +82,6 @@ action="?/updateProfile" enctype="multipart/form-data" use:enhance={({ formData }) => { - - // Agregar archivo si existe - if (avatarFile) { - formData.append('avatar', avatarFile); - } saving = true; error = ''; @@ -131,38 +99,29 @@ Foto de Perfil - Actualiza tu imagen de perfil + Esta imagen se administra desde Workspace
-
-
- -

- Haz clic en la imagen o selecciona un archivo. JPG, PNG o GIF. Máximo 2MB. -

-
+

+ La foto de perfil se sincroniza desde Workspace en login, refresh de sesión y carga de perfil. +

+

+ Para cambiarla, actualízala en Workspace y vuelve a iniciar sesión. +

From 96fd14582de3d6f978a025efacde6e164d9598cf Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Fri, 8 May 2026 17:37:27 -0500 Subject: [PATCH 7/7] fix: update down_revision for workspace profile fields migration --- .../c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py b/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py index 7608f783..446e9e31 100644 --- a/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py +++ b/backend/alembic/versions/c3d4e5f6a7b_add_workspace_profile_fields_to_user_tenants.py @@ -12,7 +12,7 @@ import sqlalchemy as sa from alembic import op revision: str = "c3d4e5f6a7b" -down_revision: Union[str, None] = "b2c3d4e5f6a7" +down_revision: Union[str, None] = "ca7d3c4e8b2a" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None