# 🚀 Phase 2 Infrastructure - EN COURS

**Date de démarrage**: 2026-01-14
**Version cible**: 0.4.0
**Objectif**: Ajouter PostgreSQL + Redis/RQ worker pour persistence et scraping asynchrone

---

## 📊 Vue d'Ensemble

### Mises a jour recentes
- Migration Alembic corrigee (down_revision sur 20260114_02)
- Extraction images Amazon amelioree (data-a-dynamic-image + filtre logos)
- Nouveau scraping de validation (URL Amazon ASUS A16)

### Prochaines actions
- Verifier l'affichage des images, description, specs, msrp et reduction dans le Web UI
- Confirmer que le popup ajout produit affiche toutes les donnees du preview

### Objectifs Phase 2
- ✅ Configuration centralisée (database, Redis, app)
- ✅ Modèles SQLAlchemy ORM (5 tables)
- ✅ Connexion base de données (init_db, get_session)
- ✅ Migrations Alembic
- ✅ Repository pattern (CRUD)
- ✅ Worker RQ pour scraping asynchrone
- ✅ Scheduler pour jobs récurrents
- ✅ CLI étendu (commandes DB + worker)
- ✅ Docker Compose (PostgreSQL + Redis)
- ✅ Gestion erreurs Redis
- ✅ Logs d'observabilité jobs
- ⏳ Tests end-to-end (Semaine 4)

---

## ✅ Semaine 1: Database Foundation (TERMINÉE)

### Tâches Complétées

#### 1. Configuration Centralisée ✅
**Fichier**: `pricewatch/app/core/config.py` (187 lignes)

**Contenu**:
- `DatabaseConfig`: Configuration PostgreSQL
  - Host, port, database, user, password
  - Propriété `url`: SQLAlchemy connection string
  - Propriété `url_async`: AsyncPG connection string (futur)
  - Prefix env vars: `PW_DB_*` (PW_DB_HOST, PW_DB_PORT, etc.)

- `RedisConfig`: Configuration Redis pour RQ
  - Host, port, db, password (optional)
  - Propriété `url`: Redis connection string
  - Prefix env vars: `PW_REDIS_*`

- `AppConfig`: Configuration globale application
  - Debug mode
  - Worker timeout (300s par défaut)
  - Worker concurrency (2 par défaut)
  - Feature flags: `enable_db`, `enable_worker`
  - Defaults Playwright: timeout, use_playwright
  - Nested configs: `db`, `redis`
  - Prefix env vars: `PW_*`

- **Pattern Singleton**: `get_config()`, `set_config()`, `reset_config()`

**Justifications**:
- 12-factor app: configuration via env vars
- Pydantic validation garantit config valide au démarrage
- Valeurs par défaut pour développement local
- Support `.env` file pour faciliter le setup
- Feature flags permettent de désactiver DB/worker pour tests

#### 2. Dépendances Phase 2 ✅
**Fichier**: `pyproject.toml` (lignes 48-60)

**Ajouts**:
```toml
# Database (Phase 2)
"sqlalchemy>=2.0.0",
"psycopg2-binary>=2.9.0",
"alembic>=1.13.0",

# Configuration (Phase 2)
"python-dotenv>=1.0.0",

# Worker/Queue (Phase 2)
"redis>=5.0.0",
"rq>=1.15.0",
"rq-scheduler>=0.13.0",
```

#### 3. Modèles SQLAlchemy ORM ✅
**Fichier**: `pricewatch/app/db/models.py` (322 lignes)

**Tables créées**:

1. **`products`** - Catalogue produits
   - PK: `id` (Integer, autoincrement)
   - Natural key: `(source, reference)` - Unique constraint
   - Colonnes: `url`, `title`, `category`, `currency`
   - Timestamps: `first_seen_at`, `last_updated_at`
   - Relations: `price_history`, `images`, `specs`, `logs`
   - Indexes: source, reference, last_updated_at

2. **`price_history`** - Historique prix (time-series)
   - PK: `id` (Integer, autoincrement)
   - FK: `product_id` → products(id) CASCADE
   - Unique: `(product_id, fetched_at)` - Évite doublons
   - Colonnes: `price` (Numeric 10,2), `shipping_cost`, `stock_status`
   - Fetch metadata: `fetch_method`, `fetch_status`, `fetched_at`
   - Check constraints: stock_status, fetch_method, fetch_status
   - Indexes: product_id, fetched_at

3. **`product_images`** - Images produit
   - PK: `id` (Integer, autoincrement)
   - FK: `product_id` → products(id) CASCADE
   - Unique: `(product_id, image_url)` - Évite doublons
   - Colonnes: `image_url` (Text), `position` (Integer, 0=main)
   - Index: product_id

4. **`product_specs`** - Caractéristiques produit (key-value)
   - PK: `id` (Integer, autoincrement)
   - FK: `product_id` → products(id) CASCADE
   - Unique: `(product_id, spec_key)` - Évite doublons
   - Colonnes: `spec_key` (String 200), `spec_value` (Text)
   - Indexes: product_id, spec_key

5. **`scraping_logs`** - Logs observabilité
   - PK: `id` (Integer, autoincrement)
   - FK optionnelle: `product_id` → products(id) SET NULL
   - Colonnes: `url`, `source`, `reference`, `fetched_at`
   - Métriques: `duration_ms`, `html_size_bytes`
   - Fetch metadata: `fetch_method`, `fetch_status`
   - Debug data (JSONB): `errors`, `notes`
   - Indexes: product_id, source, fetched_at, fetch_status

**Justifications schéma**:
- Normalisation: products séparée de price_history (catalogue vs time-series)
- Clé naturelle (source, reference) vs UUID arbitraire
- Tables séparées pour images/specs: évite JSONB non structuré
- JSONB uniquement pour données variables: errors, notes dans logs
- Cascade DELETE: suppression produit → suppression historique
- SET NULL pour logs: garde trace même si produit supprimé

---

### Tâches Complétées (suite)

#### 4. Connexion Base de Données ✅
**Fichier**: `pricewatch/app/db/connection.py`

**Contenu**:
- `get_engine(config)`: Engine SQLAlchemy (pooling)
- `get_session_factory(config)`: Session factory
- `get_session(config)`: Context manager
- `init_db(config)`: Création tables
- `check_db_connection(config)`: Health check
- `reset_engine()`: Reset pour tests

**Justifications**:
- Singleton engine pour éviter les pools multiples
- `pool_pre_ping` pour robustesse
- Context manager pour rollback/close automatiques

---

#### 5. Setup Alembic ✅
**Fichiers**:
- `alembic.ini`
- `pricewatch/app/db/migrations/env.py`
- `pricewatch/app/db/migrations/script.py.mako`

**Justifications**:
- URL DB injectée depuis `AppConfig`
- `compare_type=True` pour cohérence des migrations

#### 6. Migration Initiale ✅
**Fichier**: `pricewatch/app/db/migrations/versions/20260114_01_initial_schema.py`

**Contenu**:
- 5 tables + indexes + contraintes
- JSONB pour `errors` et `notes`

#### 7. Commandes CLI Database ✅
**Fichier**: `pricewatch/app/cli/main.py`

**Commandes**:
```bash
pricewatch init-db              # Créer tables
pricewatch migrate "message"    # Générer migration Alembic
pricewatch upgrade              # Appliquer migrations
pricewatch downgrade            # Rollback migration
```

#### 8. Docker Compose ✅
**Fichier**: `docker-compose.yml`

**Services**:
- PostgreSQL 16 (port 5432)
- Redis 7 (port 6379)
- Volumes pour persistence

#### 9. Fichier .env Exemple ✅
**Fichier**: `.env.example`

**Variables**:
```bash
# Database
PW_DB_HOST=localhost
PW_DB_PORT=5432
PW_DB_DATABASE=pricewatch
PW_DB_USER=pricewatch
PW_DB_PASSWORD=pricewatch

# Redis
PW_REDIS_HOST=localhost
PW_REDIS_PORT=6379
PW_REDIS_DB=0

# App
PW_DEBUG=false
PW_WORKER_TIMEOUT=300
PW_WORKER_CONCURRENCY=2
PW_ENABLE_DB=true
PW_ENABLE_WORKER=true
```

#### 10. Tests Database ✅
**Fichiers**:
- `tests/db/test_models.py`: Tests des modèles SQLAlchemy
- `tests/db/test_connection.py`: Tests connexion et session

**Stratégie tests**:
- SQLite in-memory pour tests unitaires
- Fixtures pytest pour setup/teardown
- Tests relationships, constraints, indexes

---

## 📦 Semaine 2: Repository & Pipeline (TERMINEE)

### Tâches Prévues

#### Repository Pattern
**Fichier**: `pricewatch/app/db/repository.py`

**Classe**: `ProductRepository`
- `get_or_create(source, reference)`: Trouver ou créer produit
- `save_snapshot(snapshot)`: Persist ProductSnapshot to DB
- `update_product_metadata(product, snapshot)`: Update title, url, etc.
- `add_price_history(product, snapshot)`: Ajouter entrée prix
- `sync_images(product, images)`: Sync images (add new, keep existing)
- `sync_specs(product, specs)`: Sync specs (upsert)
- `add_scraping_log(snapshot, product_id)`: Log scraping

**Statut**: ✅ Terminé

#### Scraping Pipeline
**Fichier**: `pricewatch/app/scraping/pipeline.py`

**Classe**: `ScrapingPipeline`
- `process_snapshot(snapshot, save_to_db)`: Orchestration
- Non-blocking: échec DB ne crash pas pipeline
- Retour: `product_id` ou `None`

**Statut**: ✅ Terminé

#### CLI Modification
**Fichier**: `pricewatch/app/cli/main.py`

**Modification commande `run`**:
- Ajouter flag `--save-db / --no-db`
- Intégrer `ScrapingPipeline` si `save_db=True`
- Compatibilité backward: JSON output toujours créé

**Statut**: ✅ Terminé

#### Tests Repository + Pipeline ✅
**Fichiers**:
- `tests/db/test_repository.py`
- `tests/scraping/test_pipeline.py`

**Statut**: ✅ Terminé

#### Tests end-to-end CLI + DB ✅
**Fichier**:
- `tests/cli/test_run_db.py`

**Statut**: ✅ Terminé

---

## 📦 Semaine 3: Worker Infrastructure (TERMINEE)

### Tâches Prévues

#### RQ Task
**Fichier**: `pricewatch/app/tasks/scrape.py`

**Fonction**: `scrape_product(url, use_playwright=True)`
- Réutilise 100% code Phase 1 (detect → fetch → parse)
- Save to DB via ScrapingPipeline
- Retour: `{success, product_id, snapshot, error}`

**Statut**: ✅ Terminé

#### Scheduler
**Fichier**: `pricewatch/app/tasks/scheduler.py`

**Classe**: `ScrapingScheduler`
- `schedule_product(url, interval_hours=24)`: Job récurrent
- `enqueue_immediate(url)`: Job unique
- Basé sur `rq-scheduler`

**Statut**: ✅ Terminé

#### CLI Worker
**Nouvelles commandes**:
```bash
pricewatch worker               # Lancer worker RQ
pricewatch enqueue <url>        # Enqueue scrape immédiat
pricewatch schedule <url> --interval 24  # Scrape quotidien
```

**Statut**: ✅ Terminé

#### Tests worker + scheduler ✅
**Fichiers**:
- `tests/tasks/test_scrape_task.py`
- `tests/tasks/test_scheduler.py`

**Statut**: ✅ Terminé

#### Gestion erreurs Redis ✅
**Fichiers modifiés**:
- `pricewatch/app/tasks/scheduler.py`:
  - Ajout `RedisUnavailableError` exception
  - Ajout `check_redis_connection()` helper
  - Connexion lazy avec ping de vérification
- `pricewatch/app/cli/main.py`:
  - Commandes `worker`, `enqueue`, `schedule` gèrent Redis down
  - Messages d'erreur clairs avec instructions

**Tests ajoutés** (7 tests):
- `test_scheduler_redis_connection_error`
- `test_scheduler_lazy_connection`
- `test_check_redis_connection_success`
- `test_check_redis_connection_failure`
- `test_scheduler_schedule_redis_error`

**Statut**: ✅ Terminé

#### Logs d'observabilité jobs ✅
**Fichier modifié**: `pricewatch/app/tasks/scrape.py`

**Logs ajoutés**:
- `[JOB START]` - Début du job avec URL
- `[STORE]` - Store détecté
- `[FETCH]` - Résultat fetch HTTP/Playwright (durée, taille)
- `[PARSE]` - Résultat parsing (titre, prix)
- `[JOB OK]` / `[JOB FAILED]` - Résultat final avec durée totale

**Note**: Les logs sont aussi persistés en DB via `ScrapingLog` (déjà implémenté).

**Statut**: ✅ Terminé

---

## 📦 Semaine 4: Tests & Documentation (EN COURS)

### Tâches Prévues

#### Tests
- ✅ Tests end-to-end (CLI → DB → Worker)
- ✅ Tests erreurs (DB down, Redis down)
- ✅ Tests backward compatibility (`--no-db`)
- ✅ Performance tests (100+ produits)

**Fichiers tests ajoutes**:
- `tests/cli/test_worker_cli.py`
- `tests/cli/test_enqueue_schedule_cli.py`
- `tests/scraping/test_pipeline.py` (erreurs DB)
- `tests/tasks/test_redis_errors.py`
- `tests/cli/test_run_no_db.py`
- `tests/db/test_bulk_persistence.py`
- `tests/tasks/test_worker_end_to_end.py`
- `tests/cli/test_cli_worker_end_to_end.py`
  - **Resultat**: OK avec Redis actif

#### Documentation
- ✅ Update README.md (setup Phase 2)
- ✅ Update CHANGELOG.md
- ✅ Migration guide (JSON → DB)

---

## 📈 Métriques d'Avancement

| Catégorie | Complétées | Totales | % |
|-----------|------------|---------|---|
| **Semaine 1** | 10 | 10 | 100% |
| **Semaine 2** | 5 | 5 | 100% |
| **Semaine 3** | 6 | 6 | 100% |
| **Semaine 4** | 7 | 7 | 100% |
| **TOTAL Phase 2** | 28 | 28 | **100%** |

---

## 🎯 Prochaine Étape Immédiate

**Prochaine étape immédiate**
- Phase 2 terminee, bascule vers Phase 3 (API REST)
- API v1 avancee: filtres, export CSV/JSON, webhooks + tests associes

**Après (prévu)**
- Documentation Phase 2 (resume final)
- Retry policy (optionnel)
- Phase 4 Web UI (dashboard + graphiques)

---

## 🔧 Vérifications

### Vérification Semaine 1 (objectif)
```bash
# Setup infrastructure
docker-compose up -d
pricewatch init-db

# Vérifier tables créées
psql -h localhost -U pricewatch pricewatch
\dt
# → 5 tables: products, price_history, product_images, product_specs, scraping_logs
```

### Vérification Semaine 2 (objectif)
```bash
# Test pipeline avec DB
pricewatch run --yaml scrap_url.yaml --save-db

# Vérifier données en DB
psql -h localhost -U pricewatch pricewatch
SELECT * FROM products LIMIT 5;
SELECT * FROM price_history ORDER BY fetched_at DESC LIMIT 10;
```

### Vérification Semaine 3 (objectif)
```bash
# Enqueue job
pricewatch enqueue "https://www.amazon.fr/dp/B08N5WRWNW"

# Lancer worker
pricewatch worker

# Vérifier job traité
psql -h localhost -U pricewatch pricewatch
SELECT * FROM scraping_logs ORDER BY fetched_at DESC LIMIT 5;
```

---

## 📝 Notes Importantes

### Backward Compatibility
- ✅ CLI Phase 1 fonctionne sans changement
- ✅ Format JSON identique
- ✅ Database optionnelle (`--no-db` flag)
- ✅ ProductSnapshot inchangé
- ✅ Tests Phase 1 continuent à passer (295 tests)

### Architecture Décisions

**Normalisation vs Performance**:
- Choix: Normalisation stricte (5 tables)
- Justification: Catalogue change rarement, prix changent quotidiennement
- Alternative rejetée: Tout dans products + JSONB (moins queryable)

**Clé Naturelle vs UUID**:
- Choix: `(source, reference)` comme unique constraint
- Justification: ASIN Amazon déjà unique globalement
- Alternative rejetée: UUID artificiel (complexifie déduplication)

**Synchrone vs Asynchrone**:
- Choix: RQ synchrone (pas d'async/await)
- Justification: Code Phase 1 réutilisable à 100%, simplicité
- Alternative rejetée: Asyncio + asyncpg (refactoring massif)

---

**Dernière mise à jour**: 2026-01-15

### Recap avancement recent (Phase 3 API)
- Filtres avances + exports CSV/JSON + webhooks (CRUD + test)
- Tests API avances ajoutes
- Nettoyage warnings Pydantic/datetime/selectors
- Suite pytest complete: 339 passed, 4 skipped

### Validation locale (Semaine 1)
```bash
docker compose up -d
./venv/bin/alembic -c alembic.ini upgrade head
psql -h localhost -U pricewatch pricewatch
\\dt
```

**Resultat**: 6 tables visibles (products, price_history, product_images, product_specs, scraping_logs, alembic_version).
**Statut**: ✅ Semaine 1 terminee (100%).
