# /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