# 💰 Web Sentinel - Guide de Monétisation Stripe

*Implémentation complète du système de paiement et gestion d'abonnements*

## 🎯 Objectif

Mettre en place un système de monétisation robuste avec **Stripe** pour gérer les abonnements FREE/PRO/ENTERPRISE et atteindre l'objectif de **170k€ ARR en 18 mois**.

---

## 📊 Modèle de monétisation

### **Tiers d'abonnement détaillés**

| Fonctionnalité | FREE | PRO (9.99€/mois) | ENTERPRISE (49.99€/mois) |
|---|---|---|---|
| **Domaines scannés** | 3 max | Illimité | Illimité |
| **Tests invasifs** | ❌ | ✅ | ✅ |
| **Export HTML** | ❌ | ✅ | ✅ |
| **Historique cloud** | ❌ | ✅ | ✅ |
| **Scans programmés** | ❌ | ✅ | ✅ |
| **API REST** | ❌ | ❌ | ✅ |
| **Multi-utilisateurs** | ❌ | ❌ | ✅ |
| **Support prioritaire** | ❌ | Email | Téléphone + Email |
| **SLA** | Aucun | 99.9% | 99.99% |

### **Objectifs financiers**
- **Mois 1-6** : 500 utilisateurs FREE → 50 PRO (500€ MRR)
- **Mois 7-12** : 2000 FREE → 200 PRO + 10 ENTERPRISE (2500€ MRR)
- **Mois 13-18** : 5000 FREE → 500 PRO + 50 ENTERPRISE (7500€ MRR)
- **Objectif 18 mois** : **14k€ MRR = 170k€ ARR** 🎯

---

## 🔧 Architecture technique Stripe

### **Schéma de base de données étendu**

```sql
-- Extension des tables existantes pour Stripe
ALTER TABLE users ADD COLUMN stripe_customer_id VARCHAR(255) UNIQUE;
ALTER TABLE users ADD COLUMN subscription_tier ENUM('FREE', 'PRO', 'ENTERPRISE') DEFAULT 'FREE';
ALTER TABLE users ADD COLUMN subscription_status ENUM('active', 'trialing', 'past_due', 'canceled', 'unpaid') DEFAULT 'active';
ALTER TABLE users ADD COLUMN trial_end_date TIMESTAMP NULL;
ALTER TABLE users ADD COLUMN billing_email VARCHAR(255);

-- Table des abonnements Stripe
CREATE TABLE stripe_subscriptions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
    stripe_customer_id VARCHAR(255) NOT NULL,
    stripe_price_id VARCHAR(255) NOT NULL,
    status ENUM('active', 'trialing', 'past_due', 'canceled', 'unpaid', 'incomplete') NOT NULL,
    current_period_start TIMESTAMP NOT NULL,
    current_period_end TIMESTAMP NOT NULL,
    trial_start TIMESTAMP NULL,
    trial_end TIMESTAMP NULL,
    cancel_at_period_end BOOLEAN DEFAULT FALSE,
    canceled_at TIMESTAMP NULL,
    ended_at TIMESTAMP NULL,
    metadata JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_stripe_subscription (stripe_subscription_id),
    INDEX idx_stripe_customer (stripe_customer_id),
    INDEX idx_user_subscription (user_id, status)
);

-- Table des paiements et factures
CREATE TABLE stripe_invoices (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    stripe_invoice_id VARCHAR(255) UNIQUE NOT NULL,
    stripe_subscription_id VARCHAR(255),
    amount_paid INT NOT NULL, -- en centimes
    currency VARCHAR(3) DEFAULT 'EUR',
    status ENUM('draft', 'open', 'paid', 'void', 'uncollectible') NOT NULL,
    invoice_pdf_url VARCHAR(500),
    hosted_invoice_url VARCHAR(500),
    period_start TIMESTAMP,
    period_end TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_stripe_invoice (stripe_invoice_id),
    INDEX idx_user_invoices (user_id, created_at)
);

-- Table des événements webhook Stripe
CREATE TABLE stripe_webhook_events (
    id INT AUTO_INCREMENT PRIMARY KEY,
    stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    processed BOOLEAN DEFAULT FALSE,
    processed_at TIMESTAMP NULL,
    stripe_data JSON NOT NULL,
    error_message TEXT NULL,
    retry_count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_event_type (event_type, processed),
    INDEX idx_stripe_event (stripe_event_id)
);
```

### **Configuration Stripe**

```python
# web_sentinel/payments/stripe_config.py
import stripe
import os
from typing import Dict, List, Optional

# Configuration Stripe
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY')
STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET')

# IDs des prix Stripe (à créer dans le dashboard Stripe)
STRIPE_PRICE_IDS = {
    'PRO_MONTHLY': 'price_1234567890abcdef',      # 9.99€/mois
    'PRO_YEARLY': 'price_abcdef1234567890',       # 99.99€/an (2 mois gratuits)
    'ENTERPRISE_MONTHLY': 'price_fedcba0987654321', # 49.99€/mois
    'ENTERPRISE_YEARLY': 'price_0987654321fedcba'   # 499.99€/an (2 mois gratuits)
}

# Configuration des fonctionnalités par tier
TIER_FEATURES = {
    'FREE': {
        'max_domains': 3,
        'invasive_scans': False,
        'html_export': False,
        'cloud_history': False,
        'scheduled_scans': False,
        'api_access': False,
        'multi_users': False,
        'support_level': 'community'
    },
    'PRO': {
        'max_domains': -1,  # Illimité
        'invasive_scans': True,
        'html_export': True,
        'cloud_history': True,
        'scheduled_scans': True,
        'api_access': False,
        'multi_users': False,
        'support_level': 'email'
    },
    'ENTERPRISE': {
        'max_domains': -1,  # Illimité
        'invasive_scans': True,
        'html_export': True,
        'cloud_history': True,
        'scheduled_scans': True,
        'api_access': True,
        'multi_users': True,
        'support_level': 'priority'
    }
}
```

### **Gestionnaire d'abonnements Stripe**

```python
# web_sentinel/payments/subscription_manager.py
import stripe
from typing import Dict, Optional, List
from datetime import datetime, timedelta
import json

class StripeSubscriptionManager:
    def __init__(self, db_manager):
        self.db = db_manager
    
    def create_customer(self, user_id: int, email: str, name: str = None) -> str:
        """Crée un client Stripe et le lie à l'utilisateur"""
        try:
            # Créer client Stripe
            customer = stripe.Customer.create(
                email=email,
                name=name,
                metadata={'user_id': str(user_id)}
            )
            
            # Mettre à jour la base de données
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("""
                    UPDATE users 
                    SET stripe_customer_id = %s, billing_email = %s 
                    WHERE id = %s
                """, (customer.id, email, user_id))
            
            return customer.id
            
        except stripe.error.StripeError as e:
            raise PaymentError(f"Failed to create Stripe customer: {e}")
    
    def create_subscription(self, user_id: int, price_id: str, 
                          trial_days: int = 14) -> Dict:
        """Crée un abonnement Stripe avec période d'essai"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Récupérer le customer ID
            cursor.execute("""
                SELECT stripe_customer_id, email FROM users WHERE id = %s
            """, (user_id,))
            user = cursor.fetchone()
            
            if not user:
                raise PaymentError("User not found")
            
            stripe_customer_id = user['stripe_customer_id']
            if not stripe_customer_id:
                # Créer le customer s'il n'existe pas
                stripe_customer_id = self.create_customer(user_id, user['email'])
            
            try:
                # Créer l'abonnement avec trial
                subscription = stripe.Subscription.create(
                    customer=stripe_customer_id,
                    items=[{'price': price_id}],
                    trial_period_days=trial_days,
                    payment_behavior='default_incomplete',
                    payment_settings={'save_default_payment_method': 'on_subscription'},
                    expand=['latest_invoice.payment_intent'],
                    metadata={'user_id': str(user_id)}
                )
                
                # Déterminer le tier basé sur le price_id
                tier = self._get_tier_from_price_id(price_id)
                
                # Sauvegarder en base
                cursor.execute("""
                    INSERT INTO stripe_subscriptions 
                    (user_id, stripe_subscription_id, stripe_customer_id, stripe_price_id,
                     status, current_period_start, current_period_end, trial_start, trial_end)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
                """, (
                    user_id, subscription.id, stripe_customer_id, price_id,
                    subscription.status,
                    datetime.fromtimestamp(subscription.current_period_start),
                    datetime.fromtimestamp(subscription.current_period_end),
                    datetime.fromtimestamp(subscription.trial_start) if subscription.trial_start else None,
                    datetime.fromtimestamp(subscription.trial_end) if subscription.trial_end else None
                ))
                
                # Mettre à jour le tier utilisateur
                cursor.execute("""
                    UPDATE users 
                    SET subscription_tier = %s, subscription_status = %s,
                        trial_end_date = %s
                    WHERE id = %s
                """, (
                    tier, subscription.status,
                    datetime.fromtimestamp(subscription.trial_end) if subscription.trial_end else None,
                    user_id
                ))
                
                return {
                    'subscription_id': subscription.id,
                    'client_secret': subscription.latest_invoice.payment_intent.client_secret,
                    'status': subscription.status,
                    'trial_end': subscription.trial_end
                }
                
            except stripe.error.StripeError as e:
                raise PaymentError(f"Failed to create subscription: {e}")
    
    def _get_tier_from_price_id(self, price_id: str) -> str:
        """Détermine le tier d'abonnement basé sur le price_id"""
        if price_id in [STRIPE_PRICE_IDS['PRO_MONTHLY'], STRIPE_PRICE_IDS['PRO_YEARLY']]:
            return 'PRO'
        elif price_id in [STRIPE_PRICE_IDS['ENTERPRISE_MONTHLY'], STRIPE_PRICE_IDS['ENTERPRISE_YEARLY']]:
            return 'ENTERPRISE'
        else:
            return 'FREE'
    
    def cancel_subscription(self, user_id: int, immediate: bool = False) -> bool:
        """Annule un abonnement"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            cursor.execute("""
                SELECT stripe_subscription_id FROM stripe_subscriptions 
                WHERE user_id = %s AND status IN ('active', 'trialing')
                ORDER BY created_at DESC LIMIT 1
            """, (user_id,))
            
            sub = cursor.fetchone()
            if not sub:
                return False
            
            try:
                if immediate:
                    # Annulation immédiate
                    stripe.Subscription.delete(sub['stripe_subscription_id'])
                    new_status = 'canceled'
                else:
                    # Annulation en fin de période
                    stripe.Subscription.modify(
                        sub['stripe_subscription_id'],
                        cancel_at_period_end=True
                    )
                    new_status = 'active'  # Reste actif jusqu'à la fin
                
                # Mettre à jour la base
                cursor.execute("""
                    UPDATE stripe_subscriptions 
                    SET status = %s, cancel_at_period_end = %s, canceled_at = %s
                    WHERE stripe_subscription_id = %s
                """, (new_status, not immediate, datetime.now() if immediate else None, sub['stripe_subscription_id']))
                
                if immediate:
                    cursor.execute("""
                        UPDATE users 
                        SET subscription_tier = 'FREE', subscription_status = 'canceled'
                        WHERE id = %s
                    """, (user_id,))
                
                return True
                
            except stripe.error.StripeError as e:
                print(f"Failed to cancel subscription: {e}")
                return False
    
    def get_user_subscription_status(self, user_id: int) -> Dict:
        """Récupère le statut d'abonnement complet d'un utilisateur"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            cursor.execute("""
                SELECT u.subscription_tier, u.subscription_status, u.trial_end_date,
                       s.stripe_subscription_id, s.current_period_end, s.cancel_at_period_end
                FROM users u
                LEFT JOIN stripe_subscriptions s ON u.id = s.user_id 
                    AND s.status IN ('active', 'trialing')
                WHERE u.id = %s
                ORDER BY s.created_at DESC
                LIMIT 1
            """, (user_id,))
            
            result = cursor.fetchone()
            if not result:
                return None
            
            # Calculer les jours restants
            days_remaining = None
            if result['current_period_end']:
                days_remaining = (result['current_period_end'] - datetime.now()).days
            
            # Vérifier si en trial
            in_trial = False
            trial_days_remaining = None
            if result['trial_end_date'] and result['trial_end_date'] > datetime.now():
                in_trial = True
                trial_days_remaining = (result['trial_end_date'] - datetime.now()).days
            
            return {
                'tier': result['subscription_tier'],
                'status': result['subscription_status'],
                'in_trial': in_trial,
                'trial_days_remaining': trial_days_remaining,
                'days_remaining': days_remaining,
                'will_cancel': result['cancel_at_period_end'] or False,
                'features': TIER_FEATURES.get(result['subscription_tier'], TIER_FEATURES['FREE'])
            }

class PaymentError(Exception):
    pass
```

### **Gestionnaire de webhooks Stripe**

```python
# web_sentinel/payments/webhook_handler.py
import stripe
from flask import Flask, request, jsonify
import json
from datetime import datetime

class StripeWebhookHandler:
    def __init__(self, db_manager, subscription_manager):
        self.db = db_manager
        self.subscription_manager = subscription_manager
    
    def handle_webhook(self, payload: bytes, sig_header: str) -> Dict:
        """Point d'entrée principal pour les webhooks Stripe"""
        try:
            # Vérifier la signature Stripe
            event = stripe.Webhook.construct_event(
                payload, sig_header, STRIPE_WEBHOOK_SECRET
            )
        except ValueError:
            return {'error': 'Invalid payload'}, 400
        except stripe.error.SignatureVerificationError:
            return {'error': 'Invalid signature'}, 400
        
        # Vérifier si l'événement a déjà été traité
        if self._is_event_processed(event['id']):
            return {'status': 'already_processed'}, 200
        
        # Enregistrer l'événement
        self._log_webhook_event(event)
        
        # Traiter selon le type d'événement
        handler_map = {
            'customer.subscription.created': self._handle_subscription_created,
            'customer.subscription.updated': self._handle_subscription_updated,
            'customer.subscription.deleted': self._handle_subscription_deleted,
            'invoice.payment_succeeded': self._handle_payment_succeeded,
            'invoice.payment_failed': self._handle_payment_failed,
            'customer.subscription.trial_will_end': self._handle_trial_ending,
        }
        
        handler = handler_map.get(event['type'])
        if handler:
            try:
                result = handler(event['data']['object'])
                self._mark_event_processed(event['id'])
                return {'status': 'processed', 'result': result}, 200
            except Exception as e:
                self._mark_event_error(event['id'], str(e))
                return {'error': f'Processing failed: {e}'}, 500
        else:
            return {'status': 'ignored', 'type': event['type']}, 200
    
    def _handle_subscription_created(self, subscription: Dict) -> Dict:
        """Traite la création d'un abonnement"""
        user_id = int(subscription['metadata'].get('user_id', 0))
        if not user_id:
            raise Exception("No user_id in subscription metadata")
        
        tier = self.subscription_manager._get_tier_from_price_id(
            subscription['items']['data'][0]['price']['id']
        )
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Mettre à jour le tier utilisateur
            cursor.execute("""
                UPDATE users 
                SET subscription_tier = %s, subscription_status = %s 
                WHERE id = %s
            """, (tier, subscription['status'], user_id))
            
            # Créer ou mettre à jour l'abonnement
            cursor.execute("""
                INSERT INTO stripe_subscriptions 
                (user_id, stripe_subscription_id, stripe_customer_id, stripe_price_id,
                 status, current_period_start, current_period_end, trial_start, trial_end)
                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
                ON DUPLICATE KEY UPDATE
                status = VALUES(status),
                current_period_start = VALUES(current_period_start),
                current_period_end = VALUES(current_period_end)
            """, (
                user_id, subscription['id'], subscription['customer'],
                subscription['items']['data'][0]['price']['id'],
                subscription['status'],
                datetime.fromtimestamp(subscription['current_period_start']),
                datetime.fromtimestamp(subscription['current_period_end']),
                datetime.fromtimestamp(subscription['trial_start']) if subscription.get('trial_start') else None,
                datetime.fromtimestamp(subscription['trial_end']) if subscription.get('trial_end') else None
            ))
        
        return {'user_id': user_id, 'tier': tier, 'status': subscription['status']}
    
    def _handle_subscription_updated(self, subscription: Dict) -> Dict:
        """Traite la mise à jour d'un abonnement"""
        user_id = int(subscription['metadata'].get('user_id', 0))
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute("""
                UPDATE stripe_subscriptions 
                SET status = %s, 
                    current_period_start = %s,
                    current_period_end = %s,
                    cancel_at_period_end = %s
                WHERE stripe_subscription_id = %s
            """, (
                subscription['status'],
                datetime.fromtimestamp(subscription['current_period_start']),
                datetime.fromtimestamp(subscription['current_period_end']),
                subscription['cancel_at_period_end'],
                subscription['id']
            ))
            
            # Mettre à jour le statut utilisateur
            cursor.execute("""
                UPDATE users 
                SET subscription_status = %s 
                WHERE id = %s
            """, (subscription['status'], user_id))
        
        return {'user_id': user_id, 'status': subscription['status']}
    
    def _handle_subscription_deleted(self, subscription: Dict) -> Dict:
        """Traite la suppression d'un abonnement"""
        user_id = int(subscription['metadata'].get('user_id', 0))
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Rétrograder vers FREE
            cursor.execute("""
                UPDATE users 
                SET subscription_tier = 'FREE', subscription_status = 'canceled'
                WHERE id = %s
            """, (user_id,))
            
            cursor.execute("""
                UPDATE stripe_subscriptions 
                SET status = 'canceled', ended_at = %s
                WHERE stripe_subscription_id = %s
            """, (datetime.now(), subscription['id']))
        
        return {'user_id': user_id, 'tier': 'FREE'}
    
    def _handle_payment_succeeded(self, invoice: Dict) -> Dict:
        """Traite un paiement réussi"""
        subscription_id = invoice.get('subscription')
        if not subscription_id:
            return {'status': 'no_subscription'}
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            # Enregistrer la facture
            cursor.execute("""
                SELECT user_id FROM stripe_subscriptions 
                WHERE stripe_subscription_id = %s
            """, (subscription_id,))
            
            result = cursor.fetchone()
            if result:
                user_id = result[0]
                
                cursor.execute("""
                    INSERT INTO stripe_invoices
                    (user_id, stripe_invoice_id, stripe_subscription_id, amount_paid,
                     currency, status, invoice_pdf_url, hosted_invoice_url)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
                    ON DUPLICATE KEY UPDATE
                    status = VALUES(status),
                    amount_paid = VALUES(amount_paid)
                """, (
                    user_id, invoice['id'], subscription_id, invoice['amount_paid'],
                    invoice['currency'], invoice['status'],
                    invoice.get('invoice_pdf'), invoice.get('hosted_invoice_url')
                ))
                
                return {'user_id': user_id, 'amount': invoice['amount_paid']}
        
        return {'status': 'user_not_found'}
    
    def _handle_payment_failed(self, invoice: Dict) -> Dict:
        """Traite un échec de paiement"""
        subscription_id = invoice.get('subscription')
        
        # Ici on pourrait :
        # 1. Envoyer un email de notification
        # 2. Marquer le compte en "past_due"
        # 3. Démarrer un processus de relance
        
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            
            cursor.execute("""
                SELECT user_id FROM stripe_subscriptions 
                WHERE stripe_subscription_id = %s
            """, (subscription_id,))
            
            result = cursor.fetchone()
            if result:
                user_id = result[0]
                
                # Marquer comme en retard de paiement
                cursor.execute("""
                    UPDATE users 
                    SET subscription_status = 'past_due'
                    WHERE id = %s
                """, (user_id,))
                
                return {'user_id': user_id, 'status': 'past_due'}
        
        return {'status': 'user_not_found'}
    
    def _handle_trial_ending(self, subscription: Dict) -> Dict:
        """Traite la fin prochaine d'un trial"""
        user_id = int(subscription['metadata'].get('user_id', 0))
        
        # Ici on pourrait envoyer un email de rappel
        # ou proposer une offre spéciale
        
        return {'user_id': user_id, 'action': 'trial_reminder_sent'}
    
    def _is_event_processed(self, event_id: str) -> bool:
        """Vérifie si un événement a déjà été traité"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                SELECT processed FROM stripe_webhook_events 
                WHERE stripe_event_id = %s
            """, (event_id,))
            result = cursor.fetchone()
            return result and result[0]
    
    def _log_webhook_event(self, event: Dict):
        """Enregistre un événement webhook"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                INSERT INTO stripe_webhook_events 
                (stripe_event_id, event_type, stripe_data)
                VALUES (%s, %s, %s)
                ON DUPLICATE KEY UPDATE
                event_type = VALUES(event_type),
                stripe_data = VALUES(stripe_data)
            """, (event['id'], event['type'], json.dumps(event)))
    
    def _mark_event_processed(self, event_id: str):
        """Marque un événement comme traité"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE stripe_webhook_events 
                SET processed = TRUE, processed_at = %s
                WHERE stripe_event_id = %s
            """, (datetime.now(), event_id))
    
    def _mark_event_error(self, event_id: str, error_message: str):
        """Marque un événement en erreur"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor()
            cursor.execute("""
                UPDATE stripe_webhook_events 
                SET error_message = %s, retry_count = retry_count + 1
                WHERE stripe_event_id = %s
            """, (error_message, event_id))
```

---

## 🌐 API REST pour la monétisation

### **Endpoints de gestion d'abonnements**

```python
# web_sentinel/api/billing_endpoints.py
from flask import Blueprint, request, jsonify, g
from web_sentinel.payments.subscription_manager import StripeSubscriptionManager

billing_bp = Blueprint('billing', __name__, url_prefix='/api/v1/billing')

@billing_bp.route('/subscription/status', methods=['GET'])
@require_auth
def get_subscription_status():
    """Récupère le statut d'abonnement de l'utilisateur"""
    subscription_manager = StripeSubscriptionManager(g.db_manager)
    status = subscription_manager.get_user_subscription_status(g.current_user_id)
    
    if not status:
        return jsonify({'error': 'No subscription found'}), 404
    
    return jsonify(status)

@billing_bp.route('/subscription/create', methods=['POST'])
@require_auth
def create_subscription():
    """Crée un nouvel abonnement"""
    data = request.get_json()
    
    if not data.get('price_id'):
        return jsonify({'error': 'price_id required'}), 400
    
    subscription_manager = StripeSubscriptionManager(g.db_manager)
    
    try:
        result = subscription_manager.create_subscription(
            user_id=g.current_user_id,
            price_id=data['price_id'],
            trial_days=data.get('trial_days', 14)
        )
        
        return jsonify(result)
        
    except PaymentError as e:
        return jsonify({'error': str(e)}), 400

@billing_bp.route('/subscription/cancel', methods=['POST'])
@require_auth
def cancel_subscription():
    """Annule l'abonnement de l'utilisateur"""
    data = request.get_json()
    immediate = data.get('immediate', False)
    
    subscription_manager = StripeSubscriptionManager(g.db_manager)
    success = subscription_manager.cancel_subscription(g.current_user_id, immediate)
    
    if success:
        return jsonify({'success': True, 'immediate': immediate})
    else:
        return jsonify({'error': 'No active subscription found'}), 404

@billing_bp.route('/invoices', methods=['GET'])
@require_auth
def get_invoices():
    """Récupère les factures de l'utilisateur"""
    with g.db_manager.get_connection() as conn:
        cursor = conn.cursor(dictionary=True)
        cursor.execute("""
            SELECT stripe_invoice_id, amount_paid, currency, status,
                   invoice_pdf_url, hosted_invoice_url, created_at
            FROM stripe_invoices 
            WHERE user_id = %s 
            ORDER BY created_at DESC
            LIMIT 20
        """, (g.current_user_id,))
        
        invoices = cursor.fetchall()
        
        # Convertir les montants en euros
        for invoice in invoices:
            invoice['amount_eur'] = invoice['amount_paid'] / 100
        
        return jsonify(invoices)

@billing_bp.route('/webhook', methods=['POST'])
def stripe_webhook():
    """Endpoint pour les webhooks Stripe"""
    payload = request.get_data()
    sig_header = request.headers.get('Stripe-Signature')
    
    webhook_handler = StripeWebhookHandler(g.db_manager, g.subscription_manager)
    result, status_code = webhook_handler.handle_webhook(payload, sig_header)
    
    return jsonify(result), status_code
```

---

## 🎛️ Interface utilisateur pour la monétisation

### **Modal d'upgrade intégré**

```python
# web_sentinel/gui/upgrade_modal.py
import tkinter as tk
from tkinter import ttk, messagebox
import webbrowser
from typing import Dict, Optional

class UpgradeModal:
    def __init__(self, parent, current_tier: str = 'FREE', 
                 subscription_manager=None):
        self.parent = parent
        self.current_tier = current_tier
        self.subscription_manager = subscription_manager
        
        self.window = tk.Toplevel(parent)
        self.window.title("🚀 Upgrade Web Sentinel")
        self.window.geometry("600x500")
        self.window.transient(parent)
        self.window.grab_set()
        
        self.setup_ui()
    
    def setup_ui(self):
        """Interface du modal d'upgrade"""
        # Titre principal
        title_frame = ttk.Frame(self.window)
        title_frame.pack(fill="x", padx=20, pady=20)
        
        title_label = ttk.Label(title_frame, text="🚀 Upgrade your Web Sentinel", 
                               font=("Arial", 16, "bold"))
        title_label.pack()
        
        subtitle_label = ttk.Label(title_frame, 
                                  text="Unlock advanced security features",
                                  font=("Arial", 10))
        subtitle_label.pack()
        
        # Tableau de comparaison
        comparison_frame = ttk.Frame(self.window)
        comparison_frame.pack(fill="both", expand=True, padx=20, pady=10)
        
        self.create_comparison_table(comparison_frame)
        
        # Boutons d'action
        button_frame = ttk.Frame(self.window)
        button_frame.pack(fill="x", padx=20, pady=20)
        
        if self.current_tier == 'FREE':
            pro_btn = ttk.Button(button_frame, text="🎯 Try PRO (14 days free)", 
                                command=lambda: self.start_trial('PRO'))
            pro_btn.pack(side="left", padx=5)
            
            enterprise_btn = ttk.Button(button_frame, text="🏢 Go ENTERPRISE", 
                                       command=lambda: self.start_trial('ENTERPRISE'))
            enterprise_btn.pack(side="left", padx=5)
        
        elif self.current_tier == 'PRO':
            enterprise_btn = ttk.Button(button_frame, text="🏢 Upgrade to ENTERPRISE", 
                                       command=lambda: self.upgrade_tier('ENTERPRISE'))
            enterprise_btn.pack(side="left", padx=5)
        
        cancel_btn = ttk.Button(button_frame, text="❌ Cancel", 
                               command=self.window.destroy)
        cancel_btn.pack(side="right", padx=5)
    
    def create_comparison_table(self, parent):
        """Crée le tableau de comparaison des tiers"""
        # Headers
        headers = ["Feature", "FREE", "PRO (€9.99/mo)", "ENTERPRISE (€49.99/mo)"]
        
        for i, header in enumerate(headers):
            label = ttk.Label(parent, text=header, font=("Arial", 10, "bold"))
            label.grid(row=0, column=i, sticky="ew", padx=5, pady=5)
        
        # Features comparison
        features = [
            ("Domains scanned", "3", "Unlimited", "Unlimited"),
            ("Invasive tests", "❌", "✅", "✅"),
            ("HTML Export", "❌", "✅", "✅"),
            ("Cloud History", "❌", "✅", "✅"),
            ("Scheduled Scans", "❌", "✅", "✅"),
            ("API Access", "❌", "❌", "✅"),
            ("Multi-users", "❌", "❌", "✅"),
            ("Support", "Community", "Email", "Priority Phone")
        ]
        
        for row, (feature, free, pro, enterprise) in enumerate(features, 1):
            ttk.Label(parent, text=feature).grid(row=row, column=0, sticky="w", padx=5, pady=2)
            ttk.Label(parent, text=free).grid(row=row, column=1, padx=5, pady=2)
            ttk.Label(parent, text=pro).grid(row=row, column=2, padx=5, pady=2)
            ttk.Label(parent, text=enterprise).grid(row=row, column=3, padx=5, pady=2)
        
        # Configure grid weights
        for i in range(4):
            parent.columnconfigure(i, weight=1)
    
    def start_trial(self, tier: str):
        """Démarre un trial pour le tier spécifié"""
        if not self.subscription_manager:
            self.open_web_checkout(tier)
            return
        
        try:
            price_id = STRIPE_PRICE_IDS[f'{tier}_MONTHLY']
            result = self.subscription_manager.create_subscription(
                user_id=g.current_user_id,  # À adapter selon le contexte
                price_id=price_id,
                trial_days=14
            )
            
            if result['status'] == 'trialing':
                messagebox.showinfo("Success", 
                    f"✅ {tier} trial started! You have 14 days to explore all features.")
                self.window.destroy()
                # Recharger l'interface principale avec nouveau tier
                self.parent.reload_with_tier(tier)
            else:
                # Rediriger vers Stripe Checkout pour paiement
                self.open_stripe_checkout(result['client_secret'])
                
        except Exception as e:
            messagebox.showerror("Error", f"Failed to start trial: {e}")
    
    def upgrade_tier(self, new_tier: str):
        """Upgrade vers un tier supérieur"""
        # Logique d'upgrade (changement de subscription)
        self.open_web_checkout(new_tier)
    
    def open_web_checkout(self, tier: str):
        """Ouvre la page de checkout web"""
        # URL vers votre page de checkout web
        checkout_url = f"https://your-domain.com/checkout?tier={tier.lower()}"
        webbrowser.open(checkout_url)
        
        messagebox.showinfo("Checkout", 
            "Opening web browser for secure payment.\nClose this app and reopen after payment.")
        
        self.window.destroy()
    
    def open_stripe_checkout(self, client_secret: str):
        """Ouvre Stripe Checkout (nécessite intégration web)"""
        # Pour une app desktop, on peut :
        # 1. Ouvrir une fenêtre web intégrée
        # 2. Rediriger vers une page web
        # 3. Utiliser une popup avec Stripe Elements
        
        # Solution simple : rediriger vers web
        checkout_url = f"https://your-domain.com/checkout?client_secret={client_secret}"
        webbrowser.open(checkout_url)
        
        self.window.destroy()

# Validation des fonctionnalités par tier
class FeatureValidator:
    """Validateur des fonctionnalités selon le tier d'abonnement"""
    
    def __init__(self, subscription_manager):
        self.subscription_manager = subscription_manager
    
    def can_scan_domain(self, user_id: int, domain_count: int) -> bool:
        """Vérifie si l'utilisateur peut scanner plus de domaines"""
        status = self.subscription_manager.get_user_subscription_status(user_id)
        max_domains = status['features']['max_domains']
        
        return max_domains == -1 or domain_count < max_domains
    
    def can_use_invasive_scans(self, user_id: int) -> bool:
        """Vérifie si l'utilisateur peut utiliser les scans invasifs"""
        status = self.subscription_manager.get_user_subscription_status(user_id)
        return status['features']['invasive_scans']
    
    def can_export_html(self, user_id: int) -> bool:
        """Vérifie si l'utilisateur peut exporter en HTML"""
        status = self.subscription_manager.get_user_subscription_status(user_id)
        return status['features']['html_export']
    
    def can_access_api(self, user_id: int) -> bool:
        """Vérifie si l'utilisateur peut accéder à l'API"""
        status = self.subscription_manager.get_user_subscription_status(user_id)
        return status['features']['api_access']
    
    def enforce_feature_gate(self, user_id: int, feature: str) -> bool:
        """Enforcement général des fonctionnalités"""
        validation_map = {
            'invasive_scans': self.can_use_invasive_scans,
            'html_export': self.can_export_html,
            'api_access': self.can_access_api
        }
        
        validator = validation_map.get(feature)
        return validator(user_id) if validator else False
```

---

## 📊 Métriques et analytics de monétisation

### **Dashboard de revenus**

```python
# web_sentinel/analytics/revenue_analytics.py
from typing import Dict, List
from datetime import datetime, timedelta

class RevenueAnalytics:
    def __init__(self, db_manager):
        self.db = db_manager
    
    def get_mrr_report(self) -> Dict:
        """Calcule le MRR (Monthly Recurring Revenue)"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # MRR par tier
            cursor.execute("""
                SELECT 
                    u.subscription_tier,
                    COUNT(*) as active_subscriptions,
                    CASE 
                        WHEN u.subscription_tier = 'PRO' THEN COUNT(*) * 9.99
                        WHEN u.subscription_tier = 'ENTERPRISE' THEN COUNT(*) * 49.99
                        ELSE 0
                    END as monthly_revenue
                FROM users u
                WHERE u.subscription_status = 'active'
                AND u.subscription_tier != 'FREE'
                GROUP BY u.subscription_tier
            """)
            
            tier_data = cursor.fetchall()
            
            total_mrr = sum(row['monthly_revenue'] for row in tier_data)
            total_subscribers = sum(row['active_subscriptions'] for row in tier_data)
            
            # ARR (Annual Recurring Revenue)
            arr = total_mrr * 12
            
            return {
                'mrr': total_mrr,
                'arr': arr,
                'total_subscribers': total_subscribers,
                'tier_breakdown': tier_data
            }
    
    def get_churn_analysis(self, days: int = 30) -> Dict:
        """Analyse du churn (taux de résiliation)"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            start_date = datetime.now() - timedelta(days=days)
            
            # Utilisateurs qui ont annulé
            cursor.execute("""
                SELECT COUNT(*) as churned_users
                FROM stripe_subscriptions 
                WHERE status = 'canceled' 
                AND canceled_at >= %s
            """, (start_date,))
            
            churned = cursor.fetchone()['churned_users']
            
            # Utilisateurs actifs au début de la période
            cursor.execute("""
                SELECT COUNT(*) as active_users
                FROM users 
                WHERE subscription_status = 'active'
                AND subscription_tier != 'FREE'
                AND created_at < %s
            """, (start_date,))
            
            active_start = cursor.fetchone()['active_users']
            
            churn_rate = (churned / active_start * 100) if active_start > 0 else 0
            
            return {
                'churn_rate_percent': round(churn_rate, 2),
                'churned_users': churned,
                'active_users_start': active_start,
                'period_days': days
            }
    
    def get_conversion_funnel(self) -> Dict:
        """Analyse du funnel de conversion FREE → PRO → ENTERPRISE"""
        with self.db.get_connection() as conn:
            cursor = conn.cursor(dictionary=True)
            
            # Compter utilisateurs par tier
            cursor.execute("""
                SELECT subscription_tier, COUNT(*) as count
                FROM users 
                GROUP BY subscription_tier
            """)
            
            tier_counts = {row['subscription_tier']: row['count'] for row in cursor.fetchall()}
            
            total_users = sum(tier_counts.values())
            free_users = tier_counts.get('FREE', 0)
            pro_users = tier_counts.get('PRO', 0)
            enterprise_users = tier_counts.get('ENTERPRISE', 0)
            
            # Calcul taux de conversion
            free_to_paid_rate = ((pro_users + enterprise_users) / total_users * 100) if total_users > 0 else 0
            pro_to_enterprise_rate = (enterprise_users / (pro_users + enterprise_users) * 100) if (pro_users + enterprise_users) > 0 else 0
            
            return {
                'total_users': total_users,
                'free_users': free_users,
                'pro_users': pro_users,
                'enterprise_users': enterprise_users,
                'free_to_paid_conversion_rate': round(free_to_paid_rate, 2),
                'pro_to_enterprise_rate': round(pro_to_enterprise_rate, 2)
            }
```

---

## 🎯 Plan d'implémentation monétisation

### **Phase 1 (Semaines 1-2) : Configuration Stripe de base**
1. ✅ **Compte Stripe** : Création compte + produits + prix
2. ✅ **Webhooks** : Configuration endpoints de réception
3. ✅ **Base de données** : Tables abonnements et paiements
4. ✅ **Backend** : SubscriptionManager et WebhookHandler

### **Phase 2 (Semaines 3-4) : Interface utilisateur**
1. 🔄 **Modal d'upgrade** : Interface graphique d'abonnement
2. 🔄 **Feature gates** : Validation des fonctionnalités par tier
3. 🔄 **Page de checkout** : Interface web de paiement
4. 🔄 **Gestion d'abonnement** : Annulation, facturation

### **Phase 3 (Mois 2) : Analytics et optimisation**
1. 🔄 **Dashboard revenus** : Métriques MRR/ARR/Churn
2. 🔄 **Email marketing** : Campagnes de conversion
3. 🔄 **A/B testing** : Optimisation prix et fonctionnalités
4. 🔄 **Support client** : Outils de gestion clients payants

---

## 🚀 Objectifs de croissance

### **Métriques clés à suivre**
- **MRR** (Monthly Recurring Revenue) : Objectif 14k€/mois
- **Churn rate** : Objectif < 5% mensuel
- **LTV/CAC** : Objectif ratio > 3:1
- **Free-to-paid conversion** : Objectif > 5%

### **Stratégies de croissance**
1. **Trial gratuit 14 jours** : Réduction friction d'adoption
2. **Pricing psychologique** : 9.99€ vs 10€ (effet ancrage)
3. **Upselling automatique** : Prompts d'upgrade contextuels
4. **Parrainage** : Réduction pour recommandations réussies

---

**Cette implémentation Stripe complète supportera la croissance vers 170k€ ARR avec un système de paiement enterprise-grade !** 💰