# Plan Implementare Redis Caching - ROA2WEB

**Data Creare:** 2025-01-25
**Status:** DRAFT - Ready for Implementation
**Durata Estimată:** 2-3 ore

---

## 📋 Sumar Executiv

- **Obiectiv:** Implementare Redis caching layer pentru reducerea query-urilor repetitive la Oracle DB
- **Target:** 60-80% reducere în numărul de query-uri pentru date frecvent accesate
- **Strategie:** Caching simplu la nivel de Service layer cu invalidation manuală
- **Backward Compatibility:** ✅ Aplicația funcționează fără Redis (graceful degradation)
- **Multi-Tenant Ready:** ✅ Cache keys pregătite pentru viitoare multi-tenancy

**Infrastructure Status:**
- ✅ Redis container configurat în `docker-compose.yml` (lines 147-163)
- ✅ Backend `depends_on: roa-redis`
- ❌ Redis client library LIPSĂ
- ❌ Cod de caching LIPSĂ

---

## 🗂️ Structura Fișierelor

### Fișiere Noi (6 fișiere)

- [ ] `shared/cache/__init__.py` - Package initialization
- [ ] `shared/cache/redis_client.py` - Redis connection client (90 lines)
- [ ] `shared/cache/decorators.py` - Cache decorators (60 lines)
- [ ] `shared/cache/utils.py` - Cache key generators, helpers (40 lines)
- [ ] `shared/tests/test_redis_client.py` - Unit tests pentru Redis client (120 lines)
- [ ] `shared/tests/test_cache_decorators.py` - Unit tests pentru decorators (80 lines)

### Fișiere Modificate (7 fișiere)

- [ ] `backend/requirements.txt` - Adaugă redis>=5.0.0
- [ ] `backend/.env.example` - Adaugă REDIS_* env vars
- [ ] `backend/app/main.py` - Initialize Redis at startup
- [ ] `backend/app/services/dashboard_service.py` - Apply caching
- [ ] `backend/app/services/invoice_service.py` - Apply caching
- [ ] `backend/app/routers/dashboard.py` - Cache invalidation on mutations
- [ ] `backend/app/routers/invoices.py` - Cache invalidation on mutations

---

## 🚀 Faze de Implementare

### FAZA 1: Setup Redis Client și Connection (30 min)

**Obiectiv:** Creează Redis client singleton cu connection pooling, similar cu `OraclePool` pattern.

**Tasks:**

1. [ ] **Adaugă dependency în requirements.txt**
   - Fișier: `backend/requirements.txt`
   - Acțiune: Adaugă linia `redis>=5.0.0` după `httpx>=0.27.0`
   - Motivație: redis 5.0+ are async support nativ

2. [ ] **Creează package cache în shared/**
   - Fișiere: `shared/cache/__init__.py`
   - Acțiune: Creează directorul și fișierul de init cu exports:
     ```python
     from .redis_client import redis_cache
     from .decorators import cached, invalidate_cache

     __all__ = ['redis_cache', 'cached', 'invalidate_cache']
     ```

3. [ ] **Implementează RedisCache singleton client**
   - Fișier: `shared/cache/redis_client.py`
   - Acțiune: Creează clasa `RedisCache` similar cu `OraclePool`:
     - Singleton pattern (nu multi-instance)
     - Async redis client cu connection pooling
     - Methods: `initialize()`, `get()`, `set()`, `delete()`, `delete_pattern()`, `close()`
     - Graceful degradation: dacă Redis e down, log warning și return None
     - Connection retry cu exponential backoff
   - Template (vezi secțiunea Code Templates mai jos)

4. [ ] **Adaugă env variables în .env.example**
   - Fișier: `backend/.env.example`
   - Acțiune: Adaugă la sfârșitul fișierului:
     ```bash
     # Redis Configuration
     REDIS_URL=redis://roa-redis:6379/0
     REDIS_PASSWORD=roa2web_redis_password
     REDIS_ENABLED=true
     CACHE_DEFAULT_TTL=300
     ```

5. [ ] **Initialize Redis la startup în main.py**
   - Fișier: `backend/app/main.py`
   - Acțiune: În funcția `startup_event()`:
     ```python
     from shared.cache import redis_cache

     @app.on_event("startup")
     async def startup_event():
         # ... existing oracle pool init ...

         # Initialize Redis cache
         if os.getenv('REDIS_ENABLED', 'false').lower() == 'true':
             await redis_cache.initialize(
                 url=os.getenv('REDIS_URL'),
                 password=os.getenv('REDIS_PASSWORD')
             )

     @app.on_event("shutdown")
     async def shutdown_event():
         # ... existing oracle pool close ...
         await redis_cache.close()
     ```

**Output Verificabil:**

- [ ] `pip install -r requirements.txt` rulează fără erori
- [ ] Redis client se conectează cu succes la container
- [ ] Test manual: `python -c "import asyncio; from shared.cache import redis_cache; asyncio.run(redis_cache.initialize())"`
- [ ] Log message: "✅ Redis cache initialized successfully"

---

### FAZA 2: Cache Decorator și Helpers (30 min)

**Obiectiv:** Creează decorator `@cached()` pentru aplicare ușoară în Services.

**Tasks:**

1. [ ] **Implementează cache key generator**
   - Fișier: `shared/cache/utils.py`
   - Acțiune: Funcții helper pentru key generation:
     ```python
     import hashlib
     import json
     from typing import Any, Dict

     def make_cache_key(tenant_id: str, resource: str, **params) -> str:
         """
         Generate tenant-aware cache key
         Format: cache:{tenant_id}:{resource}:{params_hash}
         """
         params_str = json.dumps(params, sort_keys=True)
         params_hash = hashlib.md5(params_str.encode()).hexdigest()[:12]
         return f"cache:{tenant_id}:{resource}:{params_hash}"

     def extract_tenant_id(kwargs: Dict[str, Any]) -> str:
         """
         Extract tenant_id from function kwargs
         For now returns 'default', later extract from JWT token
         """
         # TODO: Extract from request.state.tenant_id when multi-tenant implemented
         return kwargs.get('tenant_id', 'default')
     ```

2. [ ] **Implementează @cached decorator**
   - Fișier: `shared/cache/decorators.py`
   - Acțiune: Decorator pentru auto-caching de funcții async:
     ```python
     from functools import wraps
     from typing import Callable, Optional
     import logging
     from .redis_client import redis_cache
     from .utils import make_cache_key, extract_tenant_id

     logger = logging.getLogger(__name__)

     def cached(resource: str, ttl: int = 300):
         """
         Cache decorator pentru funcții async

         Usage:
             @cached(resource="dashboard_summary", ttl=300)
             async def get_dashboard_summary(company: str, username: str):
                 # ... query Oracle ...
                 return data

         Args:
             resource: Resource name (e.g., 'dashboard_summary', 'invoices_list')
             ttl: Time-to-live în secunde (default: 5 min)
         """
         def decorator(func: Callable):
             @wraps(func)
             async def wrapper(*args, **kwargs):
                 # Skip cache dacă Redis e disabled
                 if not redis_cache.is_enabled():
                     return await func(*args, **kwargs)

                 # Extract tenant_id și params pentru cache key
                 tenant_id = extract_tenant_id(kwargs)
                 cache_params = {k: v for k, v in kwargs.items()
                                if k not in ['username', 'current_user']}

                 cache_key = make_cache_key(tenant_id, resource, **cache_params)

                 # Try cache GET
                 cached_value = await redis_cache.get(cache_key)
                 if cached_value is not None:
                     logger.debug(f"Cache HIT: {cache_key}")
                     return cached_value

                 # Cache MISS - execute function
                 logger.debug(f"Cache MISS: {cache_key}")
                 result = await func(*args, **kwargs)

                 # Save to cache
                 await redis_cache.set(cache_key, result, ttl=ttl)

                 return result

             return wrapper
         return decorator
     ```

3. [ ] **Implementează invalidate_cache helper**
   - Fișier: `shared/cache/decorators.py` (same file)
   - Acțiune: Helper function pentru manual invalidation:
     ```python
     async def invalidate_cache(
         tenant_id: str = "default",
         resource: Optional[str] = None
     ):
         """
         Invalidate cache entries

         Examples:
             await invalidate_cache(resource="dashboard_summary")  # clear specific resource
             await invalidate_cache()  # clear all for default tenant
         """
         if not redis_cache.is_enabled():
             return

         if resource:
             pattern = f"cache:{tenant_id}:{resource}:*"
         else:
             pattern = f"cache:{tenant_id}:*"

         await redis_cache.delete_pattern(pattern)
         logger.info(f"Cache invalidated: {pattern}")
     ```

**Output Verificabil:**

- [ ] Decorator funcționează fără erori
- [ ] Cache key format: `cache:default:dashboard_summary:abc123`
- [ ] Test unit: `pytest shared/tests/test_cache_decorators.py -v`

---

### FAZA 3: Aplicare în Endpoint-uri (45 min)

**Obiectiv:** Aplică caching în Service layer pentru dashboard și invoices.

**Tasks:**

1. [ ] **Apply @cached în DashboardService.get_complete_summary()**
   - Fișier: `backend/app/services/dashboard_service.py`
   - Acțiune: Adaugă decorator la metoda `get_complete_summary`:
     ```python
     from shared.cache import cached

     class DashboardService:
         @staticmethod
         @cached(resource="dashboard_summary", ttl=300)  # 5 min
         async def get_complete_summary(company: str, username: str):
             # ... existing implementation ...
     ```
   - Motivație: Dashboard e accesat des, datele se schimbă rar

2. [ ] **Apply @cached în DashboardService.get_trends()**
   - Fișier: `backend/app/services/dashboard_service.py`
   - Acțiune: Similar, TTL=180 (3 min pentru trends)
   - Cache key va include period: `cache:default:dashboard_trends:company-X:period-30d:abc123`

3. [ ] **Apply @cached în DashboardService.get_detailed_data()**
   - Fișier: `backend/app/services/dashboard_service.py`
   - Acțiune: TTL=60 (1 min pentru tabel detalii - se refreshează des)
   - Cache key include page, page_size, search

4. [ ] **Apply @cached în InvoiceService.get_invoices()**
   - Fișier: `backend/app/services/invoice_service.py`
   - Acțiune: TTL=60 (1 min)
   - Cache key include filter params (partner_type, date_from, date_to, etc.)

5. [ ] **Apply @cached în InvoiceService.get_invoice_summary()**
   - Fișier: `backend/app/services/invoice_service.py`
   - Acțiune: TTL=180 (3 min pentru summary)

6. [ ] **Cache invalidation în dashboard mutations (viitor)**
   - Fișier: `backend/app/routers/dashboard.py`
   - Acțiune: Pregătește cod pentru invalidation (de activat când există POST/PUT/DELETE):
     ```python
     # TODO: Activate când implementăm mutations
     # from shared.cache import invalidate_cache
     #
     # @router.post("/...")
     # async def update_dashboard_data(...):
     #     # ... update logic ...
     #     await invalidate_cache(resource="dashboard_summary")
     #     await invalidate_cache(resource="dashboard_trends")
     ```

7. [ ] **Cache invalidation în invoice mutations**
   - Fișier: `backend/app/routers/invoices.py`
   - Acțiune: Când se implementează POST/PUT/DELETE pentru invoices, invalidează:
     - `invoices_list`
     - `invoices_summary`
     - `dashboard_summary` (afectează dashboard)

**Output Verificabil:**

- [ ] Backend pornește fără erori
- [ ] First request: Cache MISS + Oracle query (măsoară timp: ~500-1000ms)
- [ ] Second request (same params): Cache HIT (măsoară timp: ~10-20ms)
- [ ] Cache hit rate > 80% după 100 requests repetitive
- [ ] Logs arată `Cache HIT/MISS` messages

---

### FAZA 4: Testing, Monitoring și Cleanup (45 min)

**Obiectiv:** Validare funcționare corectă, performance benchmarks, și documentare.

**Tasks:**

1. [ ] **Unit tests pentru RedisCache client**
   - Fișier: `shared/tests/test_redis_client.py`
   - Acțiune: Testează:
     - Connection success/failure
     - Get/Set/Delete operations
     - Pattern matching delete
     - Graceful degradation când Redis e down
   - Run: `pytest shared/tests/test_redis_client.py -v`

2. [ ] **Unit tests pentru cache decorators**
   - Fișier: `shared/tests/test_cache_decorators.py`
   - Acțiune: Testează:
     - Decorator aplică caching corect
     - Cache key generation
     - TTL respectat
     - Invalidation funcționează
   - Run: `pytest shared/tests/test_cache_decorators.py -v`

3. [ ] **Integration test în Docker**
   - Acțiune: Pornește stack complet cu `docker-compose up`
   - Verifică:
     - Backend se conectează la Redis
     - Cache funcționează end-to-end
     - Logs arată cache hits/misses

4. [ ] **Performance benchmark**
   - Tool: Apache Bench sau Python requests loop
   - Test case: 100 requests la `/api/dashboard/summary?company=X`
   - Măsoară:
     - **Without cache** (REDIS_ENABLED=false):
       - Avg response time: ~800ms
       - Total time: ~80 seconds
     - **With cache** (REDIS_ENABLED=true):
       - First request: ~800ms (MISS)
       - Next 99 requests: ~15ms (HIT)
       - Total time: ~2 seconds
       - **Improvement: 97.5%**
   - Salvează results în `shared/docs/REDIS_PERFORMANCE_BENCHMARK.md`

5. [ ] **Manual testing checklist**
   - [ ] Dashboard: Refresh multiple ori (verify cache HIT în logs)
   - [ ] Invoices: Filtrare diferită (verify cache keys unice)
   - [ ] Redis failure test: Stop Redis container, verify app funcționează (fallback la Oracle)
   - [ ] Cache invalidation: Manual invalidate via Redis CLI, verify re-query

6. [ ] **Update CLAUDE.md documentation**
   - Fișier: `CLAUDE.md`
   - Acțiune: Adaugă secțiune "Redis Caching":
     ```markdown
     ## 💾 Redis Caching

     ROA2WEB folosește Redis pentru caching layer:

     - **Client**: `shared/cache/redis_client.py` (singleton pattern)
     - **Decorator**: `@cached(resource="name", ttl=300)` în Services
     - **Cache Keys**: `cache:{tenant_id}:{resource}:{params_hash}`
     - **TTL Defaults**:
       - Dashboard summary: 5 min
       - Dashboard trends: 3 min
       - Invoices list: 1 min
       - Invoices summary: 3 min

     **Toggle cache:** Set `REDIS_ENABLED=false` în `.env`

     **Invalidate manual:**
     ```python
     from shared.cache import invalidate_cache
     await invalidate_cache(resource="dashboard_summary")
     ```

     **Performance:** 60-80% reduction în query time pentru repetitive requests
     ```

**Output Verificabil:**

- [ ] All tests pass: `pytest shared/tests/ -v`
- [ ] Performance benchmark shows >60% improvement
- [ ] Manual testing checklist complet
- [ ] Documentation updated
- [ ] Ready for code review

---

## 📊 Cache Strategy

### Resource TTLs

| Resource | TTL | Motivație |
|----------|-----|-----------|
| `dashboard_summary` | 300s (5 min) | Date agregate, se schimbă rar |
| `dashboard_trends` | 180s (3 min) | Trends calculation expensive |
| `dashboard_detailed_data` | 60s (1 min) | Tabel interactiv, refresh frecvent |
| `dashboard_performance` | 180s (3 min) | Performance metrics stabile |
| `dashboard_cashflow` | 180s (3 min) | Forecast calculation expensive |
| `dashboard_maturity` | 180s (3 min) | Maturity analysis complex |
| `invoices_list` | 60s (1 min) | Listing cu filtre, refresh frecvent |
| `invoices_summary` | 180s (3 min) | Summary stats stabile |
| `companies_list` | 600s (10 min) | Lista rareori se schimbă |
| `treasury_data` | 120s (2 min) | Trezorerie moderate changes |

**Raționament TTL:**
- Scurt (60s): Date interactive, tabel listings
- Mediu (180-300s): Calculații expensive, agregări
- Lung (600s+): Date aproape statice (companies, permissions)

### Cache Keys Pattern

**Format:** `cache:{tenant_id}:{resource}:{params_hash}`

**Exemplu concret:**
```
cache:default:dashboard_summary:company-123:abc456def789
cache:default:invoices_list:company-123:partner-CLIENTI:unpaid-true:xyz890
cache:default:dashboard_trends:company-456:period-30d:compare-true:def123
```

**Componente:**
- `cache:` - Prefix constant (pentru separare de alte Redis keys)
- `{tenant_id}` - Tenant ID (deocamdată "default", viitor: din JWT token)
- `{resource}` - Resource name (dashboard_summary, invoices_list, etc.)
- `{params_hash}` - MD5 hash (primele 12 caractere) al parametrilor sortați JSON

**Multi-Tenant Ready:**
Când se implementează multi-tenant:
1. Modifică `extract_tenant_id()` în `utils.py` să citească din `request.state.tenant_id`
2. JWT token va include `tenant_id` field
3. Cache keys automat vor fi per-tenant
4. Invalidation per-tenant: `await invalidate_cache(tenant_id="client-a")`

### Invalidation Rules

**Trigger:** Când se schimbă date în Oracle DB

| Mutation | Invalidate Resources |
|----------|---------------------|
| Invoice created/updated | `invoices_list`, `invoices_summary`, `dashboard_summary`, `dashboard_trends` |
| Payment recorded | `invoices_list`, `dashboard_summary`, `treasury_data`, `dashboard_cashflow` |
| Treasury transaction | `treasury_data`, `dashboard_summary`, `dashboard_cashflow` |
| Company settings changed | `companies_list`, `dashboard_*` (pentru acea companie) |

**Implementare:**
```python
# În router după mutation
from shared.cache import invalidate_cache

@router.post("/invoices/{invoice_id}/pay")
async def mark_invoice_paid(...):
    # ... update DB ...

    # Invalidate affected caches
    await invalidate_cache(resource="invoices_list")
    await invalidate_cache(resource="invoices_summary")
    await invalidate_cache(resource="dashboard_summary")
    await invalidate_cache(resource="treasury_data")

    return {"status": "ok"}
```

**Pattern Matching:**
```python
# Invalidate toate cache-urile pentru dashboard
await invalidate_cache(resource="dashboard")  # matches dashboard_*

# Invalidate tot pentru un tenant
await invalidate_cache(tenant_id="client-a")  # matches cache:client-a:*
```

---

## 🧪 Testing Plan

### Unit Tests

**File:** `shared/tests/test_redis_client.py`

- [ ] `test_redis_connection_success()` - Verify successful connection
- [ ] `test_redis_connection_failure_graceful()` - Redis down, no exception thrown
- [ ] `test_redis_get_set_delete()` - Basic operations
- [ ] `test_redis_delete_pattern()` - Pattern matching deletion
- [ ] `test_redis_ttl_expiration()` - Verify TTL works
- [ ] `test_redis_connection_retry()` - Exponential backoff retry

**File:** `shared/tests/test_cache_decorators.py`

- [ ] `test_cached_decorator_hit()` - Second call returns cached value
- [ ] `test_cached_decorator_miss()` - First call queries function
- [ ] `test_cache_key_generation()` - Keys format correct
- [ ] `test_cache_key_unique_params()` - Different params = different keys
- [ ] `test_invalidate_cache_pattern()` - Invalidation works
- [ ] `test_cached_disabled()` - Works when REDIS_ENABLED=false

### Integration Tests

**Setup:** `docker-compose up -d`

**Test Scenarios:**

1. **Full Stack Test:**
   - Start backend + Redis
   - Call `/api/dashboard/summary?company=123`
   - Verify: Oracle query executed (check logs)
   - Call again same endpoint
   - Verify: Cache hit (no Oracle query)

2. **Cache Invalidation Test:**
   - Call endpoint (cache populated)
   - Invalidate via `redis-cli KEYS "cache:*"` + `DEL`
   - Call endpoint again
   - Verify: Oracle query executed (cache miss)

3. **Redis Failure Test:**
   - `docker-compose stop roa-redis`
   - Call endpoint
   - Verify: App works (fallback to Oracle)
   - No error thrown
   - Logs show warning: "Redis unavailable, fallback to DB"

### Performance Benchmarks

**Tool:** Apache Bench or Python script

**Baseline (No Cache):**
```bash
# Stop Redis or set REDIS_ENABLED=false
ab -n 100 -c 10 http://localhost:8001/api/dashboard/summary?company=123
# Expected: ~800ms avg response time, 80s total
```

**With Cache:**
```bash
# Start Redis and set REDIS_ENABLED=true
ab -n 100 -c 10 http://localhost:8001/api/dashboard/summary?company=123
# Expected: ~15ms avg (after first request), ~2s total
```

**Target Metrics:**
- Cache hit rate: >90% (după warmup)
- Avg response time reduction: >60%
- Total time reduction: >75%
- Memory usage: +50-200MB (Redis)

**Save Results:** `shared/docs/REDIS_PERFORMANCE_BENCHMARK.md`

### Manual Testing Checklist

- [ ] **Dashboard Summary:**
  - [ ] First load → check logs for "Cache MISS"
  - [ ] Refresh page → check logs for "Cache HIT"
  - [ ] Change company → new cache key, "Cache MISS"

- [ ] **Invoices List:**
  - [ ] Filter: toate facturile → "Cache MISS" first time
  - [ ] Refresh → "Cache HIT"
  - [ ] Filter: doar neplatite → new key, "Cache MISS"
  - [ ] Refresh → "Cache HIT"

- [ ] **Cache Invalidation:**
  - [ ] Load dashboard (cached)
  - [ ] Redis CLI: `redis-cli KEYS "cache:*"` → see keys
  - [ ] Delete: `redis-cli DEL cache:default:dashboard_summary:*`
  - [ ] Refresh dashboard → "Cache MISS" (re-queries Oracle)

- [ ] **Redis Failure Graceful:**
  - [ ] Stop Redis: `docker-compose stop roa-redis`
  - [ ] Access dashboard → works (no crash)
  - [ ] Check logs: "Redis unavailable, using direct DB query"
  - [ ] Start Redis: `docker-compose start roa-redis`
  - [ ] Access dashboard → caching resume

- [ ] **Multi-Tenant Simulation:**
  - [ ] Load dashboard company=123 (tenant=default)
  - [ ] Load dashboard company=456 (tenant=default)
  - [ ] Verify different cache keys in Redis

---

## 🔧 Configurare Env Variables

**File:** `backend/.env`

```bash
# ============================================================================
# REDIS CONFIGURATION
# ============================================================================

# Redis Connection URL
# Development: redis://roa-redis:6379/0 (Docker network)
# Production: redis://redis-host:6379/0 or redis://localhost:6379/0
REDIS_URL=redis://roa-redis:6379/0

# Redis Password (from docker-compose secrets)
# Match with REDIS_PASSWORD in docker-compose.yml
REDIS_PASSWORD=roa2web_redis_password

# Enable/Disable Redis Caching
# Set to 'false' to disable caching (fallback to direct DB queries)
REDIS_ENABLED=true

# Default Cache TTL (seconds)
# Used when no specific TTL provided to @cached decorator
CACHE_DEFAULT_TTL=300

# Redis Connection Pool Settings (optional, defaults shown)
REDIS_MAX_CONNECTIONS=50
REDIS_SOCKET_CONNECT_TIMEOUT=5
REDIS_SOCKET_KEEPALIVE=true
```

**Docker Compose Integration:**

No changes needed! Redis container already configured in `docker-compose.yml:147-163`.

**Verify:**
```bash
docker-compose exec roa-backend env | grep REDIS
# Should show REDIS_URL, REDIS_PASSWORD, REDIS_ENABLED
```

---

## 📝 Checklist Final

### Pre-Implementation

- [ ] Read și înțeles planul complet
- [ ] Backup codebase: `git commit -am "Backup before Redis implementation"`
- [ ] Redis container rulează: `docker-compose up -d roa-redis`
- [ ] Test connection: `docker-compose exec roa-redis redis-cli ping` → PONG

### Faza 1 (Setup)

- [ ] Dependency added: `redis>=5.0.0` în requirements.txt
- [ ] Package created: `shared/cache/__init__.py`
- [ ] Redis client: `shared/cache/redis_client.py`
- [ ] Env vars added: `backend/.env.example`
- [ ] Main.py updated: Redis initialize at startup
- [ ] Test: `python -c "import asyncio; from shared.cache import redis_cache; asyncio.run(redis_cache.initialize())"`

### Faza 2 (Decorators)

- [ ] Utils created: `shared/cache/utils.py`
- [ ] Decorator created: `shared/cache/decorators.py`
- [ ] Unit tests: `shared/tests/test_cache_decorators.py`
- [ ] Test: `pytest shared/tests/test_cache_decorators.py -v`

### Faza 3 (Integration)

- [ ] Cached applied: DashboardService.get_complete_summary
- [ ] Cached applied: DashboardService.get_trends
- [ ] Cached applied: DashboardService.get_detailed_data
- [ ] Cached applied: InvoiceService.get_invoices
- [ ] Cached applied: InvoiceService.get_invoice_summary
- [ ] Backend starts: `uvicorn app.main:app --reload`
- [ ] Test: First request slow, second fast

### Faza 4 (Validation)

- [ ] Unit tests pass: `pytest shared/tests/ -v`
- [ ] Integration tests pass (Docker stack)
- [ ] Performance benchmark run (save results)
- [ ] Manual testing checklist completed
- [ ] Documentation updated: `CLAUDE.md`
- [ ] Git commit: `git add . && git commit -m "feat: implement Redis caching layer"`

### Ready for Production

- [ ] All tests green
- [ ] Performance improvement >60%
- [ ] Graceful degradation tested (Redis failure)
- [ ] Code review requested
- [ ] Merge to main branch

---

## 📚 Referințe

### Documentație Existentă

- **Docker Compose Redis Config:** `docker-compose.yml:147-163`
- **Oracle Pool Pattern:** `shared/database/oracle_pool.py` (reference for singleton pattern)
- **Backend Services:** `backend/app/services/` (where to apply caching)
- **Backend Routers:** `backend/app/routers/` (where to invalidate cache)

### Documentație Externă

- Redis Python Client: https://redis.readthedocs.io/en/stable/
- Redis Commands: https://redis.io/commands/
- FastAPI Async: https://fastapi.tiangolo.com/async/

### Debugging

**Redis CLI Access:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password
> KEYS cache:*
> GET cache:default:dashboard_summary:abc123
> DEL cache:default:dashboard_summary:abc123
> FLUSHDB  # Delete all keys (WARNING: destructive)
```

**Monitor Redis Operations:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password MONITOR
```

**Check Cache Stats:**
```bash
docker-compose exec roa-redis redis-cli -a roa2web_redis_password INFO stats
```

---

## 🎯 Code Templates

### Template: RedisCache Client (`shared/cache/redis_client.py`)

```python
"""
Redis Cache Client - Singleton pattern similar to OraclePool
Provides async Redis operations with graceful degradation
"""
import redis.asyncio as redis
import logging
import json
from typing import Optional, Any
import os

logger = logging.getLogger(__name__)

class RedisCache:
    """Singleton Redis cache client with connection pooling"""

    _instance: Optional['RedisCache'] = None
    _client: Optional[redis.Redis] = None
    _enabled: bool = False

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(RedisCache, cls).__new__(cls)
        return cls._instance

    async def initialize(
        self,
        url: str = None,
        password: str = None,
        max_connections: int = 50
    ):
        """Initialize Redis connection pool"""
        if self._client is not None:
            return

        try:
            url = url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
            password = password or os.getenv('REDIS_PASSWORD')

            self._client = await redis.from_url(
                url,
                password=password,
                encoding="utf-8",
                decode_responses=True,
                max_connections=max_connections,
                socket_connect_timeout=5,
                socket_keepalive=True
            )

            # Test connection
            await self._client.ping()

            self._enabled = True
            logger.info("✅ Redis cache initialized successfully")

        except Exception as e:
            logger.warning(f"⚠️ Redis initialization failed: {e}. Caching disabled.")
            self._enabled = False
            self._client = None

    def is_enabled(self) -> bool:
        """Check if Redis caching is enabled"""
        return self._enabled and self._client is not None

    async def get(self, key: str) -> Optional[Any]:
        """Get value from cache"""
        if not self.is_enabled():
            return None

        try:
            value = await self._client.get(key)
            if value:
                return json.loads(value)
            return None
        except Exception as e:
            logger.error(f"Redis GET error for key {key}: {e}")
            return None

    async def set(self, key: str, value: Any, ttl: int = 300):
        """Set value in cache with TTL"""
        if not self.is_enabled():
            return

        try:
            value_json = json.dumps(value, default=str)
            await self._client.setex(key, ttl, value_json)
        except Exception as e:
            logger.error(f"Redis SET error for key {key}: {e}")

    async def delete(self, key: str):
        """Delete single key"""
        if not self.is_enabled():
            return

        try:
            await self._client.delete(key)
        except Exception as e:
            logger.error(f"Redis DELETE error for key {key}: {e}")

    async def delete_pattern(self, pattern: str):
        """Delete all keys matching pattern (e.g., 'cache:default:dashboard*')"""
        if not self.is_enabled():
            return

        try:
            async for key in self._client.scan_iter(match=pattern):
                await self._client.delete(key)
            logger.debug(f"Deleted keys matching pattern: {pattern}")
        except Exception as e:
            logger.error(f"Redis DELETE_PATTERN error for {pattern}: {e}")

    async def close(self):
        """Close Redis connection"""
        if self._client:
            await self._client.close()
            self._client = None
            self._enabled = False
            logger.info("✅ Redis connection closed")

# Global singleton instance
redis_cache = RedisCache()
```

---

## ⚠️ Known Limitations & Future Work

### Current Limitations

1. **No Cache Warming:** Cache is cold on startup (first requests slow)
   - Future: Implement background task to pre-populate hot keys

2. **Manual Invalidation:** Invalidation must be coded manually in routers
   - Future: Auto-invalidation via database triggers or event system

3. **Single Tenant:** All cache keys use `tenant_id="default"`
   - Future: Extract tenant_id from JWT token when multi-tenant implemented

4. **No Cache Monitoring:** No dashboard/metrics for cache performance
   - Future: Integrate Prometheus metrics (hit/miss rate, latency, memory)

5. **Simple Serialization:** Uses JSON (no support for binary data, datetime needs str conversion)
   - Future: Consider msgpack for faster serialization

### Future Enhancements

- [ ] **Cache Warming:** Background task to pre-load hot keys at startup
- [ ] **Smart Invalidation:** Event-driven invalidation based on DB changes
- [ ] **Cache Monitoring Dashboard:** Redis metrics + hit/miss rates
- [ ] **Cache Compression:** Compress large values (>10KB) before storing
- [ ] **Multi-Level Cache:** L1 (in-memory LRU) + L2 (Redis) for ultra-fast access
- [ ] **Cache Tagging:** Tag-based invalidation instead of pattern matching

---

## 📞 Support & Questions

**Dacă întâmpini probleme:**

1. **Redis nu pornește:** Check `docker-compose logs roa-redis`
2. **Connection failed:** Verify REDIS_URL și REDIS_PASSWORD în .env
3. **Cache nu funcționează:** Verify REDIS_ENABLED=true și logs pentru errors
4. **Performance nu se îmbunătățește:** Check cache hit rate în logs

**Contact:** Claude Code Implementation Team

---

**Planul este gata pentru implementare! Începe cu FAZA 1 și urmează pașii exact cum sunt descriși.**
