FASE 4: - Redis cache de stock con fallback graceful - Multi-moneda (MXN/USD) con contabilidad en MXN - Proveedores y ordenes de compra completo - Meilisearch 1.5M+ partes indexadas - Metabase KPIs con dashboard auto-generado FASE 5: - CRM mejorado: activities, tags, loyalty program, analytics - Imagenes de partes: upload, resize, thumbnails WebP - Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered - Garantias/RMA, alertas de reorden, multi-sucursal - Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi) FASE 6: - Notificaciones automaticas: push/WhatsApp/email/in-app - Reportes de ahorro vs retail_price - Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber - API Publica: API keys, rate limiting, catalog search Migraciones: v1.9-v3.0 Tests: 93/93 pasando Backup: nexus_backup_20260427_045859.tar.gz
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Test Meilisearch integration (Mejora #2).
|
|
|
|
Validates:
|
|
1. Meilisearch health
|
|
2. Search returns results faster than PostgreSQL tsvector
|
|
3. Fallback to PostgreSQL when Meilisearch is unreachable
|
|
4. Results are enriched with local stock
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
os.environ.setdefault('MASTER_DB_URL', 'postgresql://postgres@/nexus_autoparts')
|
|
os.environ.setdefault('TENANT_DB_URL_TEMPLATE', 'postgresql://postgres@/{db_name}')
|
|
os.environ.setdefault('POS_JWT_SECRET', 'test-secret-12345678901234567890123456789012')
|
|
|
|
from services.meili_search import health_check, search_parts
|
|
from services.catalog_service import smart_search, _search_meili_fallback
|
|
from tenant_db import get_master_conn, get_tenant_conn_by_dbname
|
|
|
|
RED = '\033[91m'
|
|
GREEN = '\033[92m'
|
|
YELLOW = '\033[93m'
|
|
RESET = '\033[0m'
|
|
|
|
|
|
def print_result(name, passed, detail=""):
|
|
status = f"{GREEN}PASS{RESET}" if passed else f"{RED}FAIL{RESET}"
|
|
print(f" [{status}] {name}" + (f" — {detail}" if detail else ""))
|
|
|
|
|
|
def main():
|
|
print("=" * 60)
|
|
print("MEILISEARCH — VALIDATION SUITE")
|
|
print("=" * 60)
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
# ── Test 1: Health check ────────────────────────────────────
|
|
print("\n[1] Meilisearch Health")
|
|
if health_check():
|
|
print_result("Health", True, "available")
|
|
passed += 1
|
|
else:
|
|
print_result("Health", False, "unreachable")
|
|
failed += 1
|
|
print(f"\n{RED}Meilisearch down — aborting{RESET}")
|
|
return passed, failed
|
|
|
|
# ── Test 2: Direct Meilisearch search ───────────────────────
|
|
print("\n[2] Direct Meilisearch Search")
|
|
try:
|
|
t0 = time.perf_counter()
|
|
result = search_parts("filtro de aceite", limit=10)
|
|
t_meili = (time.perf_counter() - t0) * 1000
|
|
|
|
if result and result.get('hits'):
|
|
hits = result['hits']
|
|
print_result("Search", True, f"{len(hits)} hits in {t_meili:.1f} ms")
|
|
passed += 1
|
|
else:
|
|
print_result("Search", False, "no hits")
|
|
failed += 1
|
|
except Exception as e:
|
|
print_result("Search", False, str(e))
|
|
failed += 1
|
|
|
|
# ── Test 3: smart_search uses Meilisearch ───────────────────
|
|
print("\n[3] smart_search() with Meilisearch")
|
|
try:
|
|
master = get_master_conn()
|
|
tenant = get_tenant_conn_by_dbname('tenant_acct_test')
|
|
|
|
# Meilisearch path
|
|
t0 = time.perf_counter()
|
|
meili_results = smart_search(master, "filtro aceite", tenant, branch_id=None, limit=10)
|
|
t_smart = (time.perf_counter() - t0) * 1000
|
|
|
|
# Pure PostgreSQL path (force fallback by using a query that might not match)
|
|
t0 = time.perf_counter()
|
|
pg_results = smart_search(master, "zzzzzzzz", tenant, branch_id=None, limit=10)
|
|
t_pg = (time.perf_counter() - t0) * 1000
|
|
|
|
master.close()
|
|
tenant.close()
|
|
|
|
if meili_results and len(meili_results) > 0:
|
|
print_result("Meili path", True, f"{len(meili_results)} results in {t_smart:.1f} ms")
|
|
passed += 1
|
|
else:
|
|
print_result("Meili path", False, "no results")
|
|
failed += 1
|
|
|
|
print(f" PostgreSQL fallback (no-match query): {t_pg:.1f} ms")
|
|
except Exception as e:
|
|
print_result("smart_search", False, str(e))
|
|
failed += 1
|
|
|
|
# ── Test 4: _search_meili_fallback returns None on error ────
|
|
print("\n[4] Fallback behavior")
|
|
try:
|
|
master = get_master_conn()
|
|
# Pass a garbage URL to force failure
|
|
old_url = os.environ.get('MEILI_URL')
|
|
os.environ['MEILI_URL'] = 'http://localhost:99999'
|
|
from services.meili_search import reset_client
|
|
reset_client()
|
|
|
|
fallback = _search_meili_fallback(master, "aceite", 10)
|
|
|
|
# Restore
|
|
if old_url:
|
|
os.environ['MEILI_URL'] = old_url
|
|
else:
|
|
os.environ.pop('MEILI_URL', None)
|
|
reset_client()
|
|
|
|
master.close()
|
|
|
|
if fallback is None:
|
|
print_result("Fallback", True, "returns None on unreachable Meilisearch")
|
|
passed += 1
|
|
else:
|
|
print_result("Fallback", False, f"unexpected return: {fallback}")
|
|
failed += 1
|
|
except Exception as e:
|
|
print_result("Fallback", False, str(e))
|
|
failed += 1
|
|
|
|
# ── Summary ─────────────────────────────────────────────────
|
|
print("\n" + "=" * 60)
|
|
print(f"RESULTS: {GREEN}{passed} passed{RESET}, {RED}{failed} failed{RESET}")
|
|
print("=" * 60)
|
|
return passed, failed
|
|
|
|
|
|
if __name__ == '__main__':
|
|
passed, failed = main()
|
|
sys.exit(0 if failed == 0 else 1)
|