Segunda integración de main: trae el carril hacia EFC y, vía el PR #9, la resolución del merge anterior que ya se había hecho en esta rama. Dos conflictos: - core/config.py: adiciones en el mismo punto. Se conservan los dos bloques, con el de EFC antes del del PAC para que el diff de esta rama contra main sea sólo lo añadido. - La migración de catálogos SAT: las dos ramas la renumeraron a g1h2i3j4k5l6 y sólo difería su down_revision. Se toma la de main —c5d6e7f8a9b0, el carril EFC—, que es la publicada. Cabeza única: j5k6l7m8n9o0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
200 lines
8.3 KiB
Python
200 lines
8.3 KiB
Python
"""
|
|
Configuración centralizada de la aplicación usando Pydantic Settings
|
|
"""
|
|
|
|
from typing import List, Literal
|
|
from pydantic import field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Configuración de la aplicación"""
|
|
|
|
# Application
|
|
APP_NAME: str = "Mi Aplicación"
|
|
# Sobreescribible con APP_VERSION (Dockerfile/Jenkins: build-arg + ENV) o entorno en runtime
|
|
APP_VERSION: str = "dev-local"
|
|
DEBUG: bool = True
|
|
ENVIRONMENT: str = "development"
|
|
|
|
# Auth local para desarrollo (sin Keycloak/Hub)
|
|
# Nunca activar en producción.
|
|
DEV_LOCAL_AUTH: bool = False
|
|
DEV_LOCAL_AUTH_EMAIL: str = "dev@local.test"
|
|
DEV_LOCAL_AUTH_NAME: str = "Dev User"
|
|
DEV_LOCAL_AUTH_TENANT_ID: int = 1
|
|
DEV_LOCAL_AUTH_COMPANY_ID: int = 1
|
|
|
|
# Database - Core (Shared)
|
|
CORE_DB_HOST: str = "postgres"
|
|
CORE_DB_PORT: int = 5432
|
|
CORE_DB_NAME: str = "app_core"
|
|
CORE_DB_USER: str = "postgres"
|
|
CORE_DB_PASSWORD: str = "postgres"
|
|
|
|
# Security
|
|
SECRET_KEY: str = "change-this-secret-key-in-production"
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
|
|
# Valkey / Redis
|
|
VALKEY_URL: str = "redis://valkey:6379/0"
|
|
PERMISSION_CACHE_ENABLED: bool = True
|
|
PERMISSION_CACHE_TTL_SECONDS: int = 300
|
|
|
|
# Sesión local del CRM (patrón SIWEB) — desacopla la sesión de la app del
|
|
# token KC de 60s. Tras SSO/login se guardan los tokens KC en valkey y se emite
|
|
# una sesión local firmada (HS256) con vida por inactividad (idle) y cap
|
|
# absoluto. Así el refresh del token KC contra el Hub solo se intenta al expirar
|
|
# la sesión local (no cada ~60s), lo que elimina el bucle de login.
|
|
#
|
|
# SE RESPETA la revocación central de Keycloak: si el Hub rechaza el refresh, la
|
|
# sesión termina (no hay re-emisión local de fallback). Flag-gated para rollback:
|
|
# con SESSION_STORE_ENABLED=False el comportamiento no cambia.
|
|
SESSION_STORE_ENABLED: bool = False
|
|
SESSION_IDLE_MINUTES: int = 30
|
|
SESSION_MAX_HOURS: int = 10
|
|
|
|
# Synchronization
|
|
SYNC_SECRET_TOKEN: str = "change-this-sync-token-in-production"
|
|
CENTRAL_SERVER_URL: str = "http://localhost:8000/api/v1/core/help-center/sync/"
|
|
SPOKE_URLS: str = "" # Comma separated list of Spoke URLs for Broadcast (Hub only)
|
|
|
|
# CORS
|
|
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
|
|
|
|
# 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 = ""
|
|
|
|
# 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", "HUB_API_BASE_URL", "EFC_API_URL", mode="before")
|
|
@classmethod
|
|
def strip_quotes(cls, v: str) -> str:
|
|
if v and isinstance(v, str):
|
|
v = v.strip().strip('"').strip("'")
|
|
# Evitar que solo espacios en .env se conviertan en "/" (rompe httpx: falta protocolo).
|
|
if not v:
|
|
return ""
|
|
if not v.endswith("/"):
|
|
v += "/"
|
|
return v
|
|
return v
|
|
|
|
# External APIs
|
|
SITAR_API_URL: str = "api.sitar.aduanasoft.com:880"
|
|
COVE_API_URL: str = "https://api.vu.aduanasoft.com"
|
|
COVE_API_VERIFY_SSL: bool = False
|
|
COVE_FIEL_HASH_KEY: str = ""
|
|
COVE_FIEL_HASH_IV: str = ""
|
|
SITAR_API_USER: str = ""
|
|
SITAR_API_PASSWORD: str = ""
|
|
# SMTP Email Configuration
|
|
SMTP_HOST: str = "smtp.gmail.com"
|
|
SMTP_PORT: int = 587
|
|
SMTP_USER: str = ""
|
|
SMTP_PASSWORD: str = ""
|
|
SMTP_FROM_NAME: str = "Mi Aplicación"
|
|
SMTP_USE_TLS: bool = True
|
|
|
|
# CSV imports (layouts_csv): redis = base64 en Valkey; minio = S3 + referencia en Redis
|
|
CSV_IMPORT_STORAGE: Literal["redis", "minio"] = "minio"
|
|
S3_ENDPOINT_URL: str = "http://minio:9000"
|
|
S3_ACCESS_KEY: str = ""
|
|
S3_SECRET_KEY: str = ""
|
|
S3_BUCKET: str = "app"
|
|
S3_REGION: str = "us-east-1"
|
|
S3_USE_SSL: bool = False
|
|
# Logos, certificados, help (si no quieres MinIO aquí, pon false Y CSV_IMPORT_STORAGE=redis)
|
|
S3_FILE_STORAGE: bool = True
|
|
S3_PRESIGNED_EXPIRES_SECONDS: int = 3600
|
|
|
|
# ----- PAC Comercio Digital (timbrado CFDI) -----
|
|
# El modo NO se configura aquí: vive en fin.invoices.stamping_mode, por factura. Esta
|
|
# variable sólo define con qué valor nacen las facturas que no lo especifican.
|
|
PAC_DEFAULT_MODE: Literal["pruebas", "produccion"] = "pruebas"
|
|
PAC_HOST_TEST: str = "pruebas.comercio-digital.mx"
|
|
PAC_HOST_PROD: str = "ws.comercio-digital.mx"
|
|
PAC_USER: str = "" # header usrws
|
|
PAC_PASSWORD: str = "" # header pwdws
|
|
PAC_TIMEOUT_SECONDS: int = 15
|
|
PAC_NOTIFICATION_EMAIL: str = "" # header email, opcional
|
|
# Clave maestra que cifra las contraseñas de los CSD guardadas en la base (ver core/crypto).
|
|
# Sin ella no se pueden cargar certificados. Se genera con:
|
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
CSD_ENCRYPTION_KEY: str = ""
|
|
# Contraseña global del CSD. LEGADO: sólo se usa como respaldo si una empresa no tiene su
|
|
# propia contraseña guardada. Lo correcto es cargar el CSD por empresa.
|
|
CSD_PASSWORD: str = ""
|
|
|
|
# ── EFC (expediente electrónico) ────────────────────────────────────────────────────────
|
|
# Carril CRM -> EFC: los documentos del CRM se resguardan en el expediente de EFC.
|
|
# Los nombres son los MISMOS que usa el gateway de Anexo22 contra el mismo EFC: inventar
|
|
# otros obligaría a quien opera los dos sistemas a recordar dos juegos de variables para
|
|
# exactamente lo mismo.
|
|
# EFC_API_URL vacía = integración APAGADA. Todo el enganche es best-effort y hace no-op:
|
|
# el CRM sigue funcionando igual, guardando los archivos solo en su MinIO.
|
|
EFC_API_URL: str = ""
|
|
EFC_API_KEY: str = "" # == CRM_INTEGRATION_API_KEY del lado de EFC
|
|
EFC_API_VERIFY_SSL: bool = True
|
|
EFC_API_TIMEOUT_MS: int = 8000 # metadatos: resolver, ingest, completar
|
|
# Las subidas van aparte: 8 s no alcanzan para un archivo de 25 MB. Debe quedar POR DEBAJO
|
|
# del proxy_read_timeout del nginx de EFC (ver core/efc_client.py).
|
|
EFC_UPLOAD_TIMEOUT_MS: int = 55000
|
|
# Scaffolding de mTLS: activarlo es configuración, no código.
|
|
EFC_MTLS_CA_PATH: str = ""
|
|
EFC_MTLS_CERT_PATH: str = ""
|
|
EFC_MTLS_KEY_PATH: str = ""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=[".env", "../.env"],
|
|
case_sensitive=True,
|
|
extra="ignore",
|
|
env_file_encoding="utf-8",
|
|
)
|
|
|
|
@property
|
|
def core_database_url(self) -> str:
|
|
"""URL de conexión a la base de datos core"""
|
|
return f"postgresql://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}"
|
|
|
|
@property
|
|
def async_core_database_url(self) -> str:
|
|
"""URL de conexión asíncrona a la base de datos core"""
|
|
return f"postgresql+asyncpg://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}"
|
|
|
|
@property
|
|
def cors_origins_list(self) -> List[str]:
|
|
"""Lista de orígenes CORS permitidos"""
|
|
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
|
|
|
|
@property
|
|
def use_s3_object_storage(self) -> bool:
|
|
"""
|
|
Usar MinIO para logos, certificados y Help (mismo bucket que CSV).
|
|
True si los imports CSV ya usan MinIO o si S3_FILE_STORAGE está activo.
|
|
"""
|
|
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()
|