FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica

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
This commit is contained in:
Nexus Dev
2026-04-27 05:23:30 +00:00
parent b70cb3042b
commit 9ff3dc4c8b
71 changed files with 10939 additions and 420 deletions

View File

@@ -0,0 +1,207 @@
#!/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)