# backend/CLAUDE.md - kaiFarma Backend

## Quick Context

**Stack Local**: FastAPI + SQLAlchemy + SQLite (instalación farmacia)
**Stack Hub**: FastAPI + PostgreSQL 15 + Redis 7 (servidor central)
**Core**: ERP parsers, CIMA snapshots, enrichment local, License validation

### Arquitectura Híbrida (Pivot 2026)
- **Local**: Backend embebido en kaiFarma.exe, SQLite, single-worker
- **Hub**: API de licencias, fleet management, snapshots CIMA, equivalencias
- **Auth**: License Key + PIN (no JWT/OAuth en local)
- **Datos ERP**: Conexión directa BD (preferido) o parsing de archivos (dropzones)

---

## ⚡ Quick Commands

| Comando | Descripción |
|---------|-------------|
| `docker-compose exec backend bash` | Shell interactivo en backend container |
| `docker-compose exec backend pytest backend/tests/` | Ejecutar tests backend |
| `docker-compose exec backend alembic upgrade head` | Aplicar migraciones pendientes |
| `docker-compose exec backend alembic revision --autogenerate -m "msg"` | Crear nueva migración |
| `docker-compose exec backend python -m backend.scripts.sync_cima` | Sync CIMA manual |
| `docker-compose logs -f backend` | Ver logs en tiempo real |
| `docker-compose restart backend` | Reiniciar backend (fix cache SQLAlchemy) |
| `psql -h localhost -U xfarma_user -d xfarma_db` | Conectar a PostgreSQL local |

**Passwords**: Dev `xfarma_dev_2024` | Test `xfarma_test_2024` (puerto 5433)

---

## 📁 Backend Structure

```
backend/
├── app/
│   ├── api/              # 26 routers, 161 endpoints REST
│   ├── models/           # 23 modelos SQLAlchemy
│   ├── services/         # 37 servicios (core + análisis)
│   ├── parsers/          # Multi-ERP (Farmatic, Farmanager, Nixfarma)
│   ├── external_data/    # CIMA/nomenclator integration
│   ├── schemas/          # Pydantic (request/response)
│   ├── core/             # Config, auth, dependencies
│   └── utils/            # Helpers (dates, safety_guards)
├── tests/                # 135+ test files (75%+ coverage target)
├── alembic/              # Migraciones DB (ver REGLA #14)
└── scripts/              # Maintenance (sync CIMA, backfill)
```

**Ver**: `docs/DATA_CATALOG.md` (modelos), `frontend/docs/API_ENDPOINTS.md` (endpoints)

---

## Critical Backend Patterns - Quick Reference

| Patrón | Cuándo Usar | Antipatrón Común | Fix Rápido |
|--------|-------------|------------------|------------|
| **Development Pharmacy** | Tests, dev local | Crear múltiples pharmacies | Usar `DEV00000000` (NIF) |
| **SQLite Local** | Instalación farmacia | PostgreSQL hardcoded | Usar `DATABASE_URL` configurable |
| **ERP Connector** | Conexión BD ERP | Solo parsing archivos | `ERPConnector(connection_string)` |
| **Códigos Nacionales String** | SIEMPRE | `Integer` types | `String(20)` en models |
| **Parser ERP Base** | Nuevos parsers | Parsers ad-hoc | `class NewParser(BaseParser)` |
| **License Validation** | Auth local | JWT complejo | `validate_pin()` + `check_license()` |
| **Offline-First** | Acceso a datos | API calls externos | SQLite snapshots locales |
| **TDD Obligatorio** | Servicios/APIs nuevos | Tests después | Tests ANTES/CON código |

### Nota Arquitectura
- **Local**: Single-worker, SQLite, sin necesidad de thread-safe registry
- **Hub**: Multi-worker posible, usar PostgreSQL + registry pattern

---

## 🛠️ Critical Backend Patterns - Details

### 1. Development Pharmacy Convention
```python
# SIEMPRE usar esta pharmacy para desarrollo y tests
DEV_PHARMACY_NIF = "DEV00000000"  # Pharmacy única de desarrollo

# ✅ CORRECTO: Tests usan pharmacy predefinida
def test_my_feature(client, dev_pharmacy):
    response = client.get(f"/api/v1/sales/{dev_pharmacy.id}")

# ❌ INCORRECTO: Crear múltiples pharmacies en tests
def test_my_feature(client):
    pharmacy = Pharmacy(nif="RANDOM123")  # NO
```

**Nota Pivot 2026**: En instalación local, cada farmacia tiene su propia instancia (single-tenant). El modelo multi-pharmacy solo aplica al Hub para gestión de flota.

**Ver**: `backend/tests/conftest.py` fixture `dev_pharmacy`

---

### 2. Database Configuration (Local vs Hub)

**Instalación Local (SQLite)**:
```python
# config/database.py - Detecta entorno automáticamente
import os

if os.getenv("KAIFARMA_LOCAL"):
    # SQLite para instalación en farmacia
    DATABASE_URL = "sqlite:///data/kaifarma.db"
    engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
else:
    # PostgreSQL para Hub o desarrollo
    DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://...")
    engine = create_engine(DATABASE_URL, pool_pre_ping=True)
```

**Hub (PostgreSQL + Multi-worker)**:
```python
# Thread-safe para multi-worker (solo Hub)
from sqlalchemy.orm import registry, scoped_session, sessionmaker

mapper_registry = registry()
Base = mapper_registry.generate_base()

SessionLocal = scoped_session(
    sessionmaker(autocommit=False, autoflush=False, bind=engine)
)
```

**Nota**: En local single-worker, `scoped_session` no es estrictamente necesario pero no hace daño.

---

### 3. Códigos Nacionales SIEMPRE String
```python
# ✅ CORRECTO
class ProductCatalog(Base):
    codigo_nacional = Column(String(20), index=True)  # String SIEMPRE

# ❌ INCORRECTO
class ProductCatalog(Base):
    codigo_nacional = Column(Integer)  # NUNCA Integer (pierde leading zeros)
```

**Razón**: Códigos nacionales tienen leading zeros (ej: 0612345). Integer los pierde.

---

### 4. Parser ERP Base Pattern
```python
# ✅ CORRECTO: Heredar de BaseParser
from backend.parsers.base import BaseParser

class FarmanagerParser(BaseParser):
    """Parser para ERP Farmanager."""

    def parse(self, df: pd.DataFrame) -> pd.DataFrame:
        # Implementar lógica específica
        return df

# ❌ INCORRECTO: Parser standalone
def parse_farmanager(filepath):  # Sin base class
    df = pd.read_csv(filepath)
    return df
```

**Ver**: `backend/parsers/base.py` para interface completa

---

### 5. Brand Detection Service (Issue #453)
**Servicio integrado** en el pipeline de enrichment para detectar marcas en productos venta libre.

```python
# Integración automática en EnrichmentService (3 puntos)
# - Línea 451: _enrich_sales_chunk() - llamada principal
# - Línea 958: _try_enrich_from_cima() - tras match CIMA
# - Línea 1578: _enrich_from_cima() - fallback

# Campos populados en SalesEnrichment:
class SalesEnrichment(Base):
    detected_brand = Column(String(100))      # "isdin", "gum", etc.
    brand_line = Column(String(100))          # "fotoprotector", etc.
    ml_subcategory = Column(String(100))      # Subcategoría ML
```

**Backfill de productos existentes**:
```bash
# Verificar cobertura actual
curl -X GET "/api/v1/reenrichment/redetect-brands/status"
# Respuesta: {"total_venta_libre": 52982, "with_brand_detected": 46332, "coverage_percentage": 87.4}

# Ejecutar backfill (solo admin)
curl -X POST "/api/v1/reenrichment/redetect-brands" -H "Authorization: Bearer $TOKEN"
# Respuesta: {"status": "initiated", "records_to_process": 6650}
```

**Configuración**: 138 marcas en `brand_detection_service.py` | Coverage target: 85%+

---

### 6. Clasificación L1/L2 NECESIDAD (ADR-004)
**Taxonomía curada** para productos venta libre (reemplaza clustering dinámico):

```python
# Categorías L1 (NECESIDAD) - 21 categorías principales
L1_CATEGORIES = ["dolor_fiebre", "respiratorio", "digestivo", "piel", "bucal", ...]

# Categorías L2 (Subcategorías) - 28 subcategorías para 6 L1
L1_WITH_L2 = {"dermocosmetica", "suplementos", "higiene_bucal", "infantil", "sexual", "control_peso"}

# Flujo de clasificación en EnrichmentService:
# 1. Parser ERP extrae datos de ventas
# 2. CIMA/Nomenclator enriquece con datos oficiales
# 3. BrandDetectionService detecta marca (138+ marcas)
# 4. ml_category (L1) asignado por reglas de keywords
# 5. ml_subcategory (L2) asignado para 6 categorías específicas
```

**Gestión**: Admin → Grupos Curados (no usar ClusteringService - deprecated)
**Ver**: `docs/architecture/ADR-004-curated-groups-l2-subcategories.md`

---

## 📊 Services & Models - Summary

### Core Services (38 total)
**Auth & Users**: `auth_service`, `user_pharmacy_service`, `invitation_service`, `subscription_expiration_service`
**File Processing**: `file_processing_service`, `enrichment_service`, `reenrichment_service`, `manual_review_service`
**Feedback Loop**: `feedback_service_v2` (Issue #457: Human-in-the-Loop para ProductCatalogVentaLibre)
**External Data**: `catalog_maintenance_service`, `catalog_sync_history_service`, `cimavet_sync_service`
**Analysis**: `generic_opportunities_service`, `partner_analysis_service`, `prescription_classification_service`, `homogeneous_groups_service`, `optimal_partners_service`, ~~`clustering_service`~~ *(deprecated ADR-004)*, `intercambiable_group_service`
**Venta Libre**: `ventalibre_service` (Issue #461: Dashboard OTC por NECESIDAD)
**Prescription**: `prescription_analytics_service`, `atc_backfill_service`, `prescription_reference_service`, `prescription_category_backfill_service`
**Employees**: `employee_service`
**Caching**: `enrichment_cache`, `laboratory_cache_service`
**Brand Detection**: `brand_detection_service`
**Health & Resilience**: `health_monitoring_service`, `cima_circuit_breaker`, `cima_retry_strategy`, `cima_sync_monitor`, `heartbeat_tracker`
**Admin**: `audit_service`, `database_management_service`, `notification_service`
**Startup**: `catalog_startup`

**Ver documentación completa**: `docs/AGENTS_INSTRUCTIONS.md` (37 services detallados)

### Database Models (23 total)
**Auth**: `User`, `Role`, `Invitation`
**Pharmacy**: `Pharmacy`, `PharmacyPartners`, `PharmacyHomogeneousMetrics`
**Sales**: `SalesData`, `SalesEnrichment`, `FileUpload`
**Catalog**: `ProductCatalog`, `ProductCatalogVet`, `ProductCatalogVentaLibre`, `NomenclatorLocal`, `HomogeneousGroup`, `HomogeneousGroupMaster`, `IntercambiableGroup`
**Prescription**: `PrescriptionReferenceList`
**System**: `CatalogSyncHistory`, `CIMAStallEvent`, `SystemHealth`, `SystemHealthMetric`, `SystemStatus`, `AuditLog`, `BackupMetadata`

**Ver documentación completa**: `docs/DATA_CATALOG.md` (23 models con schemas)

---

## 🔐 JWT Authentication (REGLA #7)

**CRÍTICO**: JWT requerido en TODOS los endpoints (sin bypass en ningún entorno)

```python
# ✅ CORRECTO: Endpoint protegido
from backend.app.api.deps import get_current_user

@router.get("/protected-endpoint")
async def protected_endpoint(
    current_user: User = Depends(get_current_user)  # OBLIGATORIO
):
    return {"data": "protected"}

# ❌ INCORRECTO: Endpoint sin protección
@router.get("/unprotected-endpoint")
async def unprotected_endpoint():  # NO auth
    return {"data": "public"}  # VULNERABLE
```

**Excepciones sin auth**:
- `/health`, `/metrics` (monitoring)
- `/api/v1/auth/*` (login, OAuth callbacks)
- `/docs`, `/redoc` (documentación)

**Ver**: `backend/app/api/deps.py` función `get_current_user()`

---

## 🧪 Testing Backend

```bash
# Run all tests con coverage
docker-compose exec backend pytest backend/tests/ --cov=backend/app --cov-report=html

# Run tests específicos
docker-compose exec backend pytest backend/tests/api/test_sales.py -v

# Run tests con markers
docker-compose exec backend pytest -m "not slow"
```

**Coverage Targets** (Issue #395):
- Services: 90%+ | APIs: 85%+ | Models: 80%+ | Parsers: 85%+
- **Mínimo bloqueante**: 35% (pre-commit)

**Ver**: `backend/tests/README.md` (troubleshooting test DB)

---

## 📚 Key Backend Documentation

- **API Endpoints**: `frontend/docs/API_ENDPOINTS.md` (172 endpoints documentados)
- **Database Models**: `docs/DATA_CATALOG.md` (23 models con relaciones)
- **Services**: `docs/AGENTS_INSTRUCTIONS.md` (38 services detallados)
- **Alembic Workflow**: `docs/ALEMBIC_WORKFLOW.md` (migraciones idempotentes)
- **Scaling Patterns**: `docs/SCALING_PATTERNS.md` (cache local, sync Hub)
- **Testing Patterns**: `backend/tests/README.md` (fixtures, DB setup)
- **Main Rules**: `CLAUDE.md` (18 reglas críticas)

---

## 🐛 Common Backend Errors

| Error | Causa | Solución |
|-------|-------|----------|
| `IntegrityError: UNIQUE pharmacy_id` | Crear User sin verificar | Verificar existencia ANTES (REGLA #10) |
| `ModuleNotFoundError` en prod | Import faltante en requirements.txt | `.\scripts\validate-imports.ps1` |
| `SQLAlchemy DetachedInstanceError` | Acceder a relación fuera de sesión | `with SessionLocal() as session:` |
| `Alembic duplicate column` | Migración no idempotente | Verificar existencia (REGLA #14) |
| `CIMA sync stall/hang` | Circuit breaker activado | `/api/v1/system/health/watchdog` |
| `Worker timeout` en Hub | Operación > 300s | Optimizar query, aumentar timeout |

**Ver troubleshooting completo**: `docs/DETAILED_RULES.md`

---

> **backend/CLAUDE.md** - Backend patterns for kaiFarma
> Last updated: 2026-01-08 (Pivot 2026: Arquitectura Híbrida)
