- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
116 lines
6.4 KiB
Python
116 lines
6.4 KiB
Python
import os
|
|
import secrets
|
|
import warnings
|
|
|
|
# ─── Database ──────────────────────────────────────────────────────────────
|
|
MASTER_DB_URL = os.environ.get("MASTER_DB_URL")
|
|
TENANT_DB_URL_TEMPLATE = os.environ.get("TENANT_DB_URL_TEMPLATE")
|
|
|
|
if not MASTER_DB_URL:
|
|
raise ValueError(
|
|
"MASTER_DB_URL environment variable is required. "
|
|
"Example: postgresql://user:pass@localhost/nexus_autoparts"
|
|
)
|
|
if not TENANT_DB_URL_TEMPLATE:
|
|
raise ValueError(
|
|
"TENANT_DB_URL_TEMPLATE environment variable is required. "
|
|
"Example: postgresql://user:pass@localhost/{db_name}"
|
|
)
|
|
|
|
# ─── JWT Authentication ────────────────────────────────────────────────────
|
|
JWT_SECRET = os.environ.get("POS_JWT_SECRET")
|
|
if not JWT_SECRET:
|
|
raise ValueError(
|
|
"POS_JWT_SECRET environment variable is required. "
|
|
"Generate one with: python3 -c 'import secrets; print(secrets.token_hex(32))'"
|
|
)
|
|
|
|
JWT_ACCESS_EXPIRES = 28800 # 8 hours (full shift)
|
|
JWT_REFRESH_EXPIRES = 2592000 # 30 days
|
|
|
|
# ─── PIN Security ──────────────────────────────────────────────────────────
|
|
PIN_MAX_ATTEMPTS_PER_MINUTE = 5
|
|
PIN_LOCKOUT_THRESHOLD = 10
|
|
PIN_LOCKOUT_MINUTES = 15
|
|
|
|
TENANT_TEMPLATE_DB = "tenant_template"
|
|
|
|
# ─── AI / OpenRouter ───────────────────────────────────────────────────────
|
|
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
|
|
if not OPENROUTER_API_KEY:
|
|
warnings.warn(
|
|
"OPENROUTER_API_KEY not set. AI chatbot features will be disabled.",
|
|
RuntimeWarning
|
|
)
|
|
|
|
# ─── Hermes Agent ──────────────────────────────────────────────────────────
|
|
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://192.168.10.71:8642/v1")
|
|
HERMES_API_KEY = os.environ.get("HERMES_API_KEY")
|
|
if not HERMES_API_KEY:
|
|
warnings.warn(
|
|
"HERMES_API_KEY not set. Hermes Agent integration will fall back to OpenRouter.",
|
|
RuntimeWarning
|
|
)
|
|
|
|
# ─── SMTP ──────────────────────────────────────────────────────────────────
|
|
SMTP_HOST = os.environ.get('SMTP_HOST', 'smtp.gmail.com')
|
|
SMTP_PORT = int(os.environ.get('SMTP_PORT', '587'))
|
|
SMTP_USER = os.environ.get('SMTP_USER', '')
|
|
SMTP_PASS = os.environ.get('SMTP_PASS', '')
|
|
SMTP_FROM = os.environ.get('SMTP_FROM', 'noreply@nexusautoparts.com.mx')
|
|
|
|
# ─── WhatsApp Bridge ───────────────────────────────────────────────────────
|
|
WHATSAPP_BRIDGE_URL = os.environ.get('WHATSAPP_BRIDGE_URL', 'http://localhost:21465')
|
|
WHATSAPP_BRIDGE_KEY = os.environ.get('WHATSAPP_BRIDGE_KEY')
|
|
if not WHATSAPP_BRIDGE_KEY:
|
|
warnings.warn(
|
|
"WHATSAPP_BRIDGE_KEY not set. WhatsApp integration will be disabled.",
|
|
RuntimeWarning
|
|
)
|
|
|
|
# ─── Multi-currency ────────────────────────────────────────────────────────
|
|
DEFAULT_CURRENCY = os.environ.get('DEFAULT_CURRENCY', 'MXN')
|
|
EXCHANGE_RATE_USD_MXN = float(os.environ.get('EXCHANGE_RATE_USD_MXN', '17.5'))
|
|
|
|
# ─── Redis Cache ───────────────────────────────────────────────────────────
|
|
REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
|
|
REDIS_ENABLED = os.environ.get('REDIS_ENABLED', 'true').lower() == 'true'
|
|
REDIS_STOCK_TTL = int(os.environ.get('REDIS_STOCK_TTL', '300'))
|
|
|
|
# ─── Meilisearch ───────────────────────────────────────────────────────────
|
|
MEILI_URL = os.environ.get('MEILI_URL', 'http://localhost:7700')
|
|
MEILI_API_KEY = os.environ.get('MEILI_API_KEY', '')
|
|
MEILI_ENABLED = os.environ.get('MEILI_ENABLED', 'true').lower() == 'true'
|
|
|
|
# ─── Catalog OEM Access ────────────────────────────────────────────────────
|
|
CATALOG_OEM_ENABLED = os.environ.get('CATALOG_OEM_ENABLED', 'false').lower() == 'true'
|
|
|
|
# ─── QWEN AI Fitment (private cloud server) ────────────────────────────────
|
|
QWEN_API_URL = os.environ.get('QWEN_API_URL', '')
|
|
QWEN_API_KEY = os.environ.get('QWEN_API_KEY', '')
|
|
QWEN_MODEL = os.environ.get('QWEN_MODEL', 'qwen3.6')
|
|
|
|
# ─── Búsqueda semántica (rerank / embeddings, mismo proveedor QWEN) ────────
|
|
# Consumidas por services/semantic_search.py. OJO: se leen al importar el
|
|
# módulo — cambiar el flag requiere recargar el worker, no es hot-reload.
|
|
QWEN_RERANK_MODEL = os.environ.get('QWEN_RERANK_MODEL', 'rerank')
|
|
QWEN_EMBED_MODEL = os.environ.get('QWEN_EMBED_MODEL', 'qwen3-embedding')
|
|
# Interruptor maestro (scope 'catalog': catálogo público / API / dashboard).
|
|
SEARCH_RERANK_ENABLED = os.environ.get('SEARCH_RERANK_ENABLED', 'true').lower() == 'true'
|
|
# Scope POS (caja + chat): apagado por defecto, el rerank agrega ~0.6-1.8 s a
|
|
# una búsqueda que responde en ~18 ms (ver services/semantic_search.py).
|
|
SEARCH_RERANK_POS_ENABLED = os.environ.get('SEARCH_RERANK_POS_ENABLED', 'false').lower() == 'true'
|
|
# Un valor no numérico en el .env no debe tumbar el arranque del POS.
|
|
try:
|
|
SEARCH_RERANK_TIMEOUT = float(os.environ.get('SEARCH_RERANK_TIMEOUT', '2'))
|
|
except ValueError:
|
|
SEARCH_RERANK_TIMEOUT = 2.0
|
|
try:
|
|
SEARCH_RERANK_MAX_CANDIDATES = int(os.environ.get('SEARCH_RERANK_MAX_CANDIDATES', '50'))
|
|
except ValueError:
|
|
SEARCH_RERANK_MAX_CANDIDATES = 50
|
|
|
|
|
|
# ─── Internal Cron / Job Security ──────────────────────────────────────────
|
|
INTERNAL_API_KEY = os.environ.get('INTERNAL_API_KEY', '')
|