Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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
This commit is contained in:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -108,6 +108,18 @@ def _get_user_key() -> str | None:
return USER_KEY.strip() or None
def _find_user_key(tenant_config: dict) -> str | None:
"""Return the Facturapi user key (env or sk_user_* tenant key), if any."""
user_key = _get_user_key()
if user_key:
return user_key
for key in ("facturapi_key", "cfdi_facturapi_key"):
tenant_key = (tenant_config.get(key) or "").strip()
if tenant_key.startswith("sk_user_"):
return tenant_key
return None
def _is_user_key_mode(tenant_config: dict) -> bool:
return bool(_get_user_key()) and not _get_secret_key(tenant_config)
@@ -138,7 +150,9 @@ def get_organization(org_id: str, api_key: str) -> dict:
def upload_csd(tenant_config: dict, cer_b64: str, key_b64: str, password: str) -> dict:
"""Upload CSD (Certificado de Sello Digital) to Facturapi.
cer_b64 and key_b64 are base64-encoded strings.
cer_b64 and key_b64 are base64-encoded strings. Uses the documented
PUT /organizations/{id}/certificate endpoint with the `cer`, `key` and
`password` multipart fields.
"""
api_key = get_api_key(tenant_config)
org_id = _get_org_id(tenant_config)
@@ -150,11 +164,11 @@ def upload_csd(tenant_config: dict, cer_b64: str, key_b64: str, password: str) -
url = f"{BASE_URL}/organizations/{org_id}/certificate"
files = {
"certificate": ("certificate.cer", cer_bytes, "application/octet-stream"),
"private_key": ("private_key.key", key_bytes, "application/octet-stream"),
"secret": (None, password),
"cer": ("certificate.cer", cer_bytes, "application/octet-stream"),
"key": ("private_key.key", key_bytes, "application/octet-stream"),
"password": (None, password),
}
resp = requests.post(url, auth=(api_key, ""), files=files, timeout=60)
resp = requests.put(url, auth=(api_key, ""), files=files, timeout=60)
if not resp.ok:
raise FacturapiError(
f"CSD upload failed: {resp.status_code} {resp.text[:500]}",
@@ -170,13 +184,9 @@ def _get_user_key_for_tenant(tenant_config: dict) -> str:
1. FACTURAPI_USER_KEY environment variable
2. tenant_config.facturapi_key if it starts with sk_user_
"""
user_key = _get_user_key()
user_key = _find_user_key(tenant_config)
if user_key:
return user_key
for key in ("facturapi_key", "cfdi_facturapi_key"):
tenant_key = (tenant_config.get(key) or "").strip()
if tenant_key.startswith("sk_user_"):
return tenant_key
raise FacturapiError("FACTURAPI_USER_KEY env or a Facturapi user key (sk_user_*) is required")
@@ -347,7 +357,7 @@ def get_org_status(tenant_config: dict) -> dict:
}
has_secret = bool(_get_secret_key(tenant_config))
has_user = bool(_get_user_key_for_tenant(tenant_config))
has_user = bool(_find_user_key(tenant_config))
if not has_secret and not has_user:
result["error"] = "Facturapi not configured. Set FACTURAPI_USER_KEY env or tenant_config.facturapi_secret_key"
return result
@@ -366,10 +376,10 @@ def get_org_status(tenant_config: dict) -> dict:
result["error"] = "No Facturapi key available"
return result
def _fetch():
org = get_organization(org_id, api_key)
legal = org.get("legal", {})
cert = org.get("certificate", {})
def _fetch(key=None):
org = get_organization(org_id, key or api_key)
legal = org.get("legal") or {}
cert = org.get("certificate") or {}
return {
"configured": True,
"has_csd": bool(cert.get("has_certificate")),
@@ -383,10 +393,11 @@ def get_org_status(tenant_config: dict) -> dict:
except FacturapiError as e:
# If the org reports "not configured for Live" (401) and we have a user key,
# try to backfill legal data and retry.
if e.status_code == 401 and _get_user_key_for_tenant(tenant_config):
user_key = _find_user_key(tenant_config)
if e.status_code == 401 and user_key:
try:
update_organization_legal(tenant_config, org_id)
result.update(_fetch())
result.update(_fetch(user_key))
except FacturapiError as e2:
result["error"] = str(e2)
else: