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:
120
pos/services/redis_stock_cache.py
Normal file
120
pos/services/redis_stock_cache.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# /home/Autopartes/pos/services/redis_stock_cache.py
|
||||
"""Redis cache layer for inventory stock calculations.
|
||||
|
||||
Provides sub-millisecond stock lookups by caching SUM(inventory_operations)
|
||||
results in Redis. Cache is invalidated on every stock mutation.
|
||||
|
||||
Fallback: if Redis is unavailable, queries PostgreSQL directly.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import redis
|
||||
from decimal import Decimal
|
||||
|
||||
# Connection settings from environment
|
||||
REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
|
||||
REDIS_STOCK_TTL = int(os.environ.get('REDIS_STOCK_TTL', '300')) # 5 minutes default
|
||||
REDIS_ENABLED = os.environ.get('REDIS_ENABLED', 'true').lower() == 'true'
|
||||
|
||||
# Lazy connection
|
||||
_redis_client = None
|
||||
|
||||
|
||||
def _get_redis():
|
||||
"""Get or create Redis connection (lazy singleton)."""
|
||||
global _redis_client
|
||||
if _redis_client is None and REDIS_ENABLED:
|
||||
try:
|
||||
_redis_client = redis.from_url(REDIS_URL, decode_responses=True)
|
||||
_redis_client.ping()
|
||||
except Exception as e:
|
||||
print(f"[redis_stock_cache] Redis unavailable: {e}")
|
||||
_redis_client = False # Disable for this session
|
||||
return _redis_client if _redis_client is not False else None
|
||||
|
||||
|
||||
def _stock_key(inventory_id, branch_id=None):
|
||||
"""Generate Redis key for a stock entry."""
|
||||
if branch_id:
|
||||
return f"nexus:stock:{inventory_id}:b{branch_id}"
|
||||
return f"nexus:stock:{inventory_id}"
|
||||
|
||||
|
||||
def get_cached_stock(inventory_id, branch_id=None):
|
||||
"""Get stock from Redis cache.
|
||||
|
||||
Returns:
|
||||
int/None: Stock quantity if cached, None if miss or Redis down.
|
||||
"""
|
||||
r = _get_redis()
|
||||
if not r:
|
||||
return None
|
||||
try:
|
||||
val = r.get(_stock_key(inventory_id, branch_id))
|
||||
if val is not None:
|
||||
return int(val)
|
||||
except Exception as e:
|
||||
print(f"[redis_stock_cache] GET error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def set_cached_stock(inventory_id, quantity, branch_id=None):
|
||||
"""Store stock in Redis cache with TTL."""
|
||||
r = _get_redis()
|
||||
if not r:
|
||||
return
|
||||
try:
|
||||
key = _stock_key(inventory_id, branch_id)
|
||||
r.set(key, int(quantity), ex=REDIS_STOCK_TTL)
|
||||
except Exception as e:
|
||||
print(f"[redis_stock_cache] SET error: {e}")
|
||||
|
||||
|
||||
def invalidate_stock(inventory_id, branch_id=None):
|
||||
"""Remove stock entry from Redis cache.
|
||||
|
||||
Called after any inventory mutation (sale, purchase, adjust, transfer).
|
||||
If branch_id is None, invalidates both global and branch-specific keys.
|
||||
"""
|
||||
r = _get_redis()
|
||||
if not r:
|
||||
return
|
||||
try:
|
||||
keys = [_stock_key(inventory_id)]
|
||||
if branch_id:
|
||||
keys.append(_stock_key(inventory_id, branch_id))
|
||||
else:
|
||||
# Wildcard invalidation for all branches of this item
|
||||
pattern = _stock_key(inventory_id, '*')
|
||||
keys = r.keys(pattern)
|
||||
keys.append(_stock_key(inventory_id))
|
||||
if keys:
|
||||
r.delete(*keys)
|
||||
except Exception as e:
|
||||
print(f"[redis_stock_cache] DELETE error: {e}")
|
||||
|
||||
|
||||
def invalidate_all_stock():
|
||||
"""Flush all stock keys from Redis. Use with caution (e.g., after bulk import)."""
|
||||
r = _get_redis()
|
||||
if not r:
|
||||
return
|
||||
try:
|
||||
keys = r.keys('nexus:stock:*')
|
||||
if keys:
|
||||
r.delete(*keys)
|
||||
print(f"[redis_stock_cache] Flushed {len(keys)} stock keys")
|
||||
except Exception as e:
|
||||
print(f"[redis_stock_cache] FLUSH error: {e}")
|
||||
|
||||
|
||||
def health_check():
|
||||
"""Return True if Redis is reachable."""
|
||||
r = _get_redis()
|
||||
if not r:
|
||||
return False
|
||||
try:
|
||||
return r.ping()
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user