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
208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Test Redis stock cache integration.
|
|
|
|
Validates:
|
|
1. Redis connectivity
|
|
2. Cache miss → PostgreSQL fallback → cache set
|
|
3. Cache hit (sub-millisecond)
|
|
4. Invalidation on stock mutation
|
|
5. Graceful degradation when Redis is unavailable
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import warnings
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Must set env vars before importing config
|
|
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.redis_stock_cache import (
|
|
get_cached_stock, set_cached_stock, invalidate_stock,
|
|
invalidate_all_stock, health_check, _get_redis
|
|
)
|
|
from services.inventory_engine import get_stock, record_sale, record_purchase
|
|
from tenant_db import 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("REDIS STOCK CACHE — VALIDATION SUITE")
|
|
print("=" * 60)
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
# ── Test 1: Redis connectivity ──────────────────────────────
|
|
print("\n[1] Redis Connectivity")
|
|
if health_check():
|
|
print_result("Redis PING", True, "responding")
|
|
passed += 1
|
|
else:
|
|
print_result("Redis PING", False, "no response")
|
|
failed += 1
|
|
print(f"\n{RED}Redis unavailable — aborting remaining tests{RESET}")
|
|
return passed, failed
|
|
|
|
# ── Test 2: Basic cache operations ──────────────────────────
|
|
print("\n[2] Basic Cache Operations")
|
|
set_cached_stock(99999, 42, branch_id=1)
|
|
cached = get_cached_stock(99999, branch_id=1)
|
|
if cached == 42:
|
|
print_result("SET + GET", True, f"value={cached}")
|
|
passed += 1
|
|
else:
|
|
print_result("SET + GET", False, f"expected 42, got {cached}")
|
|
failed += 1
|
|
|
|
invalidate_stock(99999, branch_id=1)
|
|
cached_after = get_cached_stock(99999, branch_id=1)
|
|
if cached_after is None:
|
|
print_result("Invalidation", True, "key removed")
|
|
passed += 1
|
|
else:
|
|
print_result("Invalidation", False, f"expected None, got {cached_after}")
|
|
failed += 1
|
|
|
|
# ── Test 3: get_stock cache miss / hit ──────────────────────
|
|
print("\n[3] get_stock() with PostgreSQL fallback")
|
|
conn = get_tenant_conn_by_dbname('tenant_acct_test')
|
|
|
|
# Ensure we have at least one inventory item with operations
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1")
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
|
|
if not row:
|
|
print(f" {YELLOW}SKIP{RESET} No inventory items in tenant_acct_test")
|
|
else:
|
|
inv_id = row[0]
|
|
|
|
# Clear any existing cache
|
|
invalidate_stock(inv_id, None)
|
|
|
|
# Miss (should query PostgreSQL)
|
|
t0 = time.perf_counter()
|
|
stock_miss = get_stock(conn, inv_id)
|
|
t_miss = (time.perf_counter() - t0) * 1000
|
|
|
|
# Hit (should read from Redis)
|
|
t0 = time.perf_counter()
|
|
stock_hit = get_stock(conn, inv_id)
|
|
t_hit = (time.perf_counter() - t0) * 1000
|
|
|
|
if stock_miss == stock_hit:
|
|
print_result("Consistency", True, f"PG={stock_miss}, Redis={stock_hit}")
|
|
passed += 1
|
|
else:
|
|
print_result("Consistency", False, f"PG={stock_miss}, Redis={stock_hit}")
|
|
failed += 1
|
|
|
|
print(f" Cache miss: {t_miss:.3f} ms")
|
|
print(f" Cache hit: {t_hit:.3f} ms")
|
|
|
|
if t_hit < t_miss:
|
|
print_result("Performance", True, f"hit {t_hit:.3f}ms < miss {t_miss:.3f}ms")
|
|
passed += 1
|
|
else:
|
|
print_result("Performance", False, "cache hit not faster")
|
|
failed += 1
|
|
|
|
conn.close()
|
|
|
|
# ── Test 4: Invalidation on mutation ────────────────────────
|
|
print("\n[4] Invalidation on stock mutation")
|
|
conn = get_tenant_conn_by_dbname('tenant_acct_test')
|
|
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id FROM inventory WHERE is_active = true LIMIT 1")
|
|
row = cur.fetchone()
|
|
cur.close()
|
|
|
|
if not row:
|
|
print(f" {YELLOW}SKIP{RESET} No inventory items available")
|
|
else:
|
|
inv_id = row[0]
|
|
|
|
# Pre-populate cache
|
|
invalidate_stock(inv_id, None)
|
|
stock_before = get_stock(conn, inv_id)
|
|
set_cached_stock(inv_id, stock_before)
|
|
|
|
# Verify cache hit
|
|
cached_before = get_cached_stock(inv_id)
|
|
if cached_before != stock_before:
|
|
print_result("Pre-populate", False, f"cache mismatch")
|
|
failed += 1
|
|
else:
|
|
# Record a sale (negative operation)
|
|
# Need a valid branch_id
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT id FROM branches LIMIT 1")
|
|
branch_row = cur.fetchone()
|
|
cur.close()
|
|
branch_id = branch_row[0] if branch_row else None
|
|
|
|
if branch_id:
|
|
record_sale(conn, inv_id, branch_id, 1)
|
|
conn.commit()
|
|
|
|
# Cache should be invalidated
|
|
cached_after = get_cached_stock(inv_id)
|
|
if cached_after is None:
|
|
print_result("Auto-invalidation", True, "cleared on record_sale")
|
|
passed += 1
|
|
else:
|
|
print_result("Auto-invalidation", False, f"still cached: {cached_after}")
|
|
failed += 1
|
|
else:
|
|
print(f" {YELLOW}SKIP{RESET} No branches available")
|
|
|
|
conn.close()
|
|
|
|
# ── Test 5: Bulk stock population ───────────────────────────
|
|
print("\n[5] get_stock_bulk() populates Redis")
|
|
conn = get_tenant_conn_by_dbname('tenant_acct_test')
|
|
from services.inventory_engine import get_stock_bulk
|
|
|
|
stock_map = get_stock_bulk(conn)
|
|
if stock_map:
|
|
sample_id = list(stock_map.keys())[0]
|
|
cached = get_cached_stock(sample_id)
|
|
if cached is not None:
|
|
print_result("Bulk populate", True, f"{len(stock_map)} items cached")
|
|
passed += 1
|
|
else:
|
|
print_result("Bulk populate", False, "sample not in cache")
|
|
failed += 1
|
|
else:
|
|
print(f" {YELLOW}SKIP{RESET} No stock data")
|
|
conn.close()
|
|
|
|
# ── 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)
|