# CLAUDE.md — DOOBAT Bot Telegram

**Copyright 2026 Vincent Gallet - Vincesoft**

Ce fichier est la référence technique complète du projet DOOBAT pour les futurs développements et la maintenance.

---

## 1. Vue d'ensemble du projet

DOOBAT est un bot Telegram professionnel connecté à Odoo Online. Il permet à des **commerciaux** et des **clients référencés** d'accéder à des informations Odoo (tarifs, stock, documents) via des demandes en langage naturel, avec génération automatique de PDF.

**URL de production :** `https://doobat.doozen.fr`
**Odoo :** `https://edu-doobat.odoo.com`

---

## 2. Architecture technique

```
┌─────────────────────────────────────────────────────────────────────┐
│                        FLUX D'UN MESSAGE                            │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Utilisateur Telegram                                               │
│       │                                                             │
│       ▼                                                             │
│  API Telegram ──POST──▶ /webhook/telegram (FastAPI)                 │
│                                │                                    │
│                    SecurityService.validate_telegram_secret()       │
│                                │                                    │
│                    SecurityService.check_rate_limit()               │
│                                │                                    │
│                    AuthService.find_partner_by_telegram()           │
│                                │                                    │
│                    OdooService.find_partner_by_id()                 │
│                                │                                    │
│                    AIService.analyze_message()                      │
│                                │                                    │
│               ┌────────────────┼────────────────┐                  │
│               │                │                │                  │
│          intent=tarif    intent=stock    intent=client_docs         │
│               │                │                │                  │
│       OdooService         OdooService      OdooService              │
│     .get_products()   .get_stock()   .get_client_docs()             │
│               │                │                │                  │
│          PDFService        PDFService       PDFService              │
│       .generate_tarif() .generate_stock() .generate_docs()         │
│               │                │                │                  │
│          TelegramService.send_pdf_to_telegram()                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 3. Structure du projet

```
doobat_bot/
├── app.py                          # Point d'entrée FastAPI + lifespan
├── config.ini                      # Configuration (NE PAS COMMITER)
├── config.ini.example              # Template de configuration
├── .env.example                    # Template variables d'environnement
├── requirements.txt                # Dépendances Python
├── README.md                       # Documentation utilisateur
├── CLAUDE.md                       # Documentation technique (ce fichier)
│
├── services/
│   ├── __init__.py
│   ├── odoo_service.py             # XML-RPC Odoo : auth, recherche, données
│   ├── ai_service.py               # OpenAI + fallback regex
│   ├── pdf_service.py              # Génération PDF ReportLab
│   ├── auth_service.py             # Association Telegram ↔ Odoo
│   ├── security_service.py         # Webhook, rate limit, sanitisation
│   ├── cleanup_service.py          # Nettoyage PDF expirés
│   └── telegram_service.py         # Envoi messages et documents Telegram
│
├── routes/
│   ├── __init__.py
│   └── telegram_webhook.py         # Route POST /webhook/telegram + logique métier
│
├── models/
│   ├── __init__.py
│   ├── intent_model.py             # Pydantic : IntentAnalysis, Intent enum
│   └── response_model.py           # Pydantic : réponses API
│
├── templates/                      # HTML Jinja2 (pour WeasyPrint si utilisé)
│   ├── base_template.html
│   ├── tarif_template.html
│   ├── stock_template.html
│   └── client_documents_template.html
│
├── static/                         # Fichiers statiques
│   └── logo.png                    # Logo DOOBAT (à placer manuellement)
│
├── pdf/                            # PDF générés (auto-nettoyés)
├── logs/                           # Logs applicatifs
├── data/
│   └── telegram_contacts.json      # Associations chat_id → partner_id
│
├── scripts/
│   ├── install.sh                  # Installation automatique
│   ├── start.sh                    # Démarrage (foreground ou daemon)
│   ├── set_webhook.py              # Configuration webhook Telegram
│   └── cleanup.sh                  # Nettoyage manuel PDF
│
└── tests/
    ├── test_odoo.py                # Tests unitaires OdooService
    ├── test_ai.py                  # Tests unitaires AIService
    └── test_telegram.py            # Tests Security + Auth
```

---

## 4. Logique métier complète

### 4.1 Flux d'identification

```
Message reçu
    │
    ├─▶ find_partner_by_telegram(chat_id)   [via telegram_contacts.json]
    │       │
    │       ├─ Trouvé ──▶ find_partner_by_id(partner_id)
    │       │
    │       └─ Non trouvé ──▶ find_partner_by_phone(user.phone_number)
    │                               │
    │                               ├─ Trouvé ──▶ auto-link + continuer
    │                               │
    │                               └─ Non trouvé ──▶ "Ce service ne vous est pas disponible."
    │
    ▼
Partenaire Odoo récupéré
    │
    ├─▶ is_commercial(partner) ?
    │       │
    │       ├─ OUI ──▶ Accès complet (tarif, stock, client_documents)
    │       │
    │       └─ NON (client) ──▶ Accès tarif uniquement
    │                           Autres demandes → "Contacter votre commercial"
```

### 4.2 Vérification d'affectation client

Pour les demandes de documents client par un commercial :

```python
# Champ Odoo : res.partner.user_id (commercial affecté)
check_client_assigned_to_commercial(client_id, commercial_user_id)
# Si False → "Ce client ne vous est pas affecté."
```

### 4.3 Identification du rôle

Le champ Odoo `res.partner.x_studio_titre_bot` détermine le rôle :
- `"Commercial"` → accès complet
- Toute autre valeur (ou vide) → rôle client

---

## 5. Services — Détail

### 5.1 OdooService (`services/odoo_service.py`)

**Connexion :** XML-RPC via `xmlrpc.client.ServerProxy`
**Authentification :** lazy + cache (l'UID est mémorisé après le premier appel)

**Cache :** `SimpleCache` (TTL=300s). Clés importantes :
- `partner_phone_{phone}` → partenaire par téléphone
- `partner_id_{id}` → partenaire par ID
- `products_{limit}_{offset}` → articles
- `stock_{name}` → données de stock
- `assigned_{client_id}_{commercial_id}` → vérification affectation

**Retry :** décorateur `@retry(max_attempts=3, delay=2.0)`

**Méthodes clés :**
| Méthode | Description |
|---------|-------------|
| `authenticate()` | Auth XML-RPC, retourne UID |
| `find_partner_by_phone(phone)` | Recherche par phone+mobile, nettoie le numéro |
| `find_partner_by_id(id)` | Récupère un partenaire par ID |
| `find_partner_by_name(name)` | Recherche ilike par nom |
| `is_commercial(partner)` | Vérifie x_studio_titre_bot |
| `check_client_assigned_to_commercial(client_id, user_id)` | Vérifie user_id |
| `get_products_paginated()` | Tous les articles par pages |
| `get_stock_for_product(name)` | Stock + emplacements |
| `get_full_client_documents(client_id, months)` | Devis + commandes + factures |

**Modèles Odoo utilisés :**
- `res.partner` — contacts/clients/commerciaux
- `product.template` — articles (catalogue)
- `product.product` — variantes articles (stock)
- `stock.quant` — quantités par emplacement
- `sale.order` — devis et commandes
- `account.move` — factures

### 5.2 AIService (`services/ai_service.py`)

**Mode OpenAI :** Envoie le message au modèle GPT avec un system prompt strict exigeant un JSON structuré (`response_format={"type": "json_object"}`).

**Fallback regex :** Toujours actif si OpenAI échoue ou n'est pas configuré. Patterns séparés pour chaque intention.

**Résultat normalisé :**
```python
{
    "intent": "tarif|stock|client_documents|actions|help|unknown",
    "client_name": "DUPONT",      # vide si non détecté
    "article_name": "REF001",     # vide si non détecté
    "confidence": 0.95,           # 0.0 à 1.0
}
```

**Sécurité :** Les messages sont tronqués à 500 caractères avant envoi à OpenAI.

### 5.3 PDFService (`services/pdf_service.py`)

**Moteur :** ReportLab (rendu vectoriel natif Python, pas de dépendance système).

**Nommage fichiers :** `{type}_{identifiant}_{uuid8}.pdf` — UUID 8 chars pour unicité, sanitisation anti path-traversal.

**3 types de PDF :**

1. **Tarif** (`generate_tarif_pdf`) — Regroupement par catégorie, tableau avec référence/désignation/type/prix
2. **Stock** (`generate_stock_pdf`) — Tableau avec stock dispo/virtuel + détail par emplacement
3. **Documents client** (`generate_client_documents_pdf`) — 3 sections : devis, commandes, factures avec totaux et badges statut

**Footer systématique :** `Copyright 2026 Vincent Gallet - Vincesoft | DOOBAT Bot Telegram` + numéro de page.

### 5.4 SecurityService (`services/security_service.py`)

**Validation webhook :** `hmac.compare_digest` pour éviter les timing attacks.

**Rate limiting :** fenêtre glissante de 60 secondes par `chat_id`. Configurable via `rate_limit_per_minute`.

**Sanitisation :** méthodes statiques pour nettoyer :
- `sanitize_text_input()` — supprime caractères de contrôle, limite longueur
- `sanitize_partner_name()` — regex whitelist
- `sanitize_article_name()` — regex whitelist
- `sanitize_phone()` — garde seulement `+0-9 -()`

### 5.5 AuthService (`services/auth_service.py`)

**Persistance :** `data/telegram_contacts.json` — écriture atomique via fichier `.tmp` + `os.replace()`.

**Format :** `{"chat_id_string": partner_id_int, ...}`

**Sécurité :** Validation stricte des types avant sauvegarde.

### 5.6 TelegramService (`services/telegram_service.py`)

**HTTP :** `aiohttp.ClientSession` (async), timeout 30s.

**Envoi document :** `multipart/form-data` avec le PDF en pièce jointe. Vérifie la taille (< 50 MB limite Telegram).

**Indicateurs de frappe :** `send_typing_action()` et `send_upload_action()` pour améliorer l'UX.

### 5.7 CleanupService (`services/cleanup_service.py`)

**Déclenché :** au démarrage de l'app + après chaque génération de PDF.

**Protection :** `os.path.realpath()` pour anti path-traversal avant suppression.

---

## 6. Route principale (`routes/telegram_webhook.py`)

### Services instanciés au niveau module

Les services sont des **singletons au niveau du module** — ils partagent leur état (cache, associations) pour toute la durée de vie du processus.

```python
_config = _load_config()
_odoo = OdooService(_config)
_ai = AIService(_config)
_pdf = PDFService(_config)
_auth = AuthService(_config)
_security = SecurityService(_config)
_telegram = TelegramService(_config)
_cleanup = CleanupService(_config)
```

### Ordre de traitement d'un message

1. Validation secret webhook → `403` si invalide
2. Parse JSON → `400` si invalide
3. Rate limiting par `chat_id`
4. Détection commande `/xxx` ou langage naturel
5. Résolution partenaire Odoo (`_resolve_partner`)
6. Vérification rôle (`is_commercial`)
7. Analyse IA (`analyze_message`)
8. Dispatch selon intention
9. Génération PDF
10. Envoi Telegram

### Résolution du partenaire

```python
async def _resolve_partner(chat_id, user):
    # 1. Association manuelle (json)
    partner_id = _auth.find_partner_by_telegram(chat_id)
    if partner_id:
        return _odoo.find_partner_by_id(partner_id)
    
    # 2. Téléphone partagé (optionnel, rarement disponible)
    phone = user.get("phone_number", "")
    if phone:
        partner = _odoo.find_partner_by_phone(phone)
        if partner:
            _auth.link_telegram_to_partner(chat_id, partner["id"])
            return partner
    
    return None  # → send_not_authorized()
```

---

## 7. Gestion IA

### Prompt système

Le prompt système injecte les contraintes strictes :
- Retourner **uniquement** du JSON valide
- Utiliser `response_format={"type": "json_object"}` (OpenAI native)
- Confiance 0.0–1.0

### Intentions supportées

| Intent | Déclencheurs exemples |
|--------|----------------------|
| `tarif` | "tarif", "prix", "catalogue", "liste des articles" |
| `stock` | "stock", "disponibilité", "quantité", "dispo" |
| `client_documents` | "factures", "commandes", "devis", "documents", "historique" |
| `actions` | "actions", "opportunités", "pipeline", "CRM" |
| `help` | "aide", "help", "/help", "que peut-on faire" |
| `unknown` | Tout le reste |

### Fallback regex

Toujours actif. Organisé en listes de patterns par intention. Ne lève jamais d'exception.

---

## 8. Sécurité — Points critiques

### Ce qui ne doit JAMAIS apparaître dans les logs

- `bot_token` (masqué : `mask_token()`)
- `password` Odoo
- `api_key` OpenAI
- `webhook_secret`
- `admin_token`

### Anti-injections

- Odoo XML-RPC : les domains sont des listes Python natives, pas des chaînes SQL
- Noms de fichiers : `re.sub(r"[^\w\-_\.]", "_", name)` + anti `../`
- Inputs utilisateur : whitelist regex + longueur limitée

### Rate limiting

Fenêtre glissante 60s. Indépendant par `chat_id`. Configurable (`rate_limit_per_minute` dans `[SECURITY]`).

---

## 9. Configuration — Sections et clés

| Section | Clé | Description |
|---------|-----|-------------|
| ODOO | url | URL Odoo Online |
| ODOO | db | Nom de la base de données |
| ODOO | username | Email administrateur |
| ODOO | password | Mot de passe Odoo |
| ODOO | timeout | Timeout XML-RPC (secondes) |
| ODOO | max_retries | Nombre de retries |
| TELEGRAM | bot_token | Token @BotFather |
| TELEGRAM | webhook_secret | Secret webhook (32+ chars) |
| OPENAI | api_key | Clé API OpenAI |
| OPENAI | model | Modèle (gpt-4o-mini recommandé) |
| SERVER | base_url | HTTPS public du bot |
| SERVER | host | Interface d'écoute (0.0.0.0) |
| SERVER | port | Port Uvicorn (8000) |
| PDF | output_dir | Dossier des PDF générés |
| PDF | max_age_days | Durée de vie PDF |
| PDF | logo_path | Chemin du logo |
| LOGS | log_dir | Dossier des logs |
| LOGS | log_level | INFO / DEBUG / WARNING |
| SECURITY | admin_token | Token administration |
| SECURITY | rate_limit_per_minute | Requêtes/minute par user |
| SECURITY | allowed_hosts | Hosts autorisés (CORS/TrustedHost) |

---

## 10. Déploiement PlanetHoster — Checklist

```
[ ] Python 3.11+ disponible sur le serveur
[ ] Environnement virtuel créé (venv/)
[ ] requirements.txt installé
[ ] config.ini complété avec les vraies valeurs
[ ] Logo placé dans static/logo.png
[ ] Nginx installé et configuré
[ ] Certificat SSL Let's Encrypt obtenu
[ ] Nginx testé (nginx -t)
[ ] DOOBAT démarré (scripts/start.sh ou systemd)
[ ] /health accessible : https://doobat.doozen.fr/health
[ ] Webhook Telegram configuré (python scripts/set_webhook.py)
[ ] Webhook vérifié (python scripts/set_webhook.py --info)
[ ] Test message /start depuis Telegram
[ ] Test /link avec un vrai numéro Odoo
[ ] Test demande tarif
```

---

## 11. Procédures de debug

### Bot ne répond pas

1. Vérifier l'état du service : `curl https://doobat.doozen.fr/health`
2. Vérifier les logs : `tail -f logs/doobat.log`
3. Vérifier le webhook : `python scripts/set_webhook.py --info`
4. Vérifier Nginx : `sudo nginx -t && sudo tail -f /var/log/nginx/doobat_error.log`

### Erreur Odoo "Authentication failed"

1. Vérifier les credentials dans `config.ini`
2. Tester manuellement :
   ```bash
   python3 -c "import xmlrpc.client; c=xmlrpc.client.ServerProxy('https://edu-doobat.odoo.com/xmlrpc/2/common'); print(c.version())"
   ```

### PDF non envoyé

1. Vérifier le dossier `pdf/` (droits d'écriture)
2. Chercher `Erreur génération PDF` dans les logs
3. Vérifier que `reportlab` est installé : `pip show reportlab`

### OpenAI retourne des erreurs

Le fallback regex prend le relais automatiquement. Vérifier :
1. Clé API valide dans `config.ini`
2. Quota OpenAI non épuisé
3. Les logs indiquent `"IA (regex fallback)"` si OpenAI est indisponible

### Rate limit trop agressif

Modifier `rate_limit_per_minute` dans `config.ini` et redémarrer.

### Contact non reconnu

1. Vérifier `data/telegram_contacts.json`
2. Demander à l'utilisateur de faire `/link +33XXXXXXXXX`
3. Vérifier que le numéro existe bien dans Odoo (`res.partner.phone` ou `.mobile`)

---

## 12. Bonnes pratiques de développement

### Ajouter une nouvelle intention

1. Ajouter l'intent dans `models/intent_model.py` (enum `Intent`)
2. Ajouter des patterns regex dans `services/ai_service.py` (`_XXXXX_PATTERNS`)
3. Mettre à jour le system prompt OpenAI dans `AIService.SYSTEM_PROMPT`
4. Ajouter le handler dans `routes/telegram_webhook.py` (`_handle_natural_language`)
5. Créer la fonction de traitement `_process_xxx()`

### Ajouter un nouveau type de PDF

1. Créer la méthode dans `services/pdf_service.py`
2. Créer le template HTML dans `templates/` (pour WeasyPrint si nécessaire)
3. Appeler depuis le webhook

### Modifier les champs Odoo

1. Modifier le domain et les `fields` dans `services/odoo_service.py`
2. Adapter l'affichage dans `services/pdf_service.py`
3. Mettre à jour le template HTML correspondant

### Ne jamais faire

- Commiter `config.ini` dans un dépôt
- Logger des tokens, mots de passe ou clés API
- Utiliser `os.path.join(user_input)` sans sanitisation
- Appeler `_execute()` avec des données utilisateur directement dans le domain sans validation

---

## 13. Évolutions possibles

| Fonctionnalité | Complexité | Prérequis |
|----------------|-----------|-----------|
| Actions commerciales CRM | Moyenne | `crm.lead` Odoo |
| Recherche multi-clients | Faible | Déjà implémenté |
| Rapport hebdomadaire automatique | Moyenne | Cron + scheduler |
| Boutons inline Telegram | Faible | `callback_query` déjà géré |
| Authentification 2FA | Haute | Code OTP + SMS |
| Multilingue | Moyenne | Jinja2 i18n |
| Dashboard web admin | Haute | React/Vue + API admin |
| Cache Redis | Moyenne | Remplacer SimpleCache |
| Celery pour tâches async | Haute | Redis + Celery |

---

*DOOBAT — Documentation technique — Copyright 2026 Vincent Gallet - Vincesoft*
