# Fichier: python_cheats/cheatsheets/architecture_trois_tiers.txt
# Cheatsheet Architecture Trois Tiers - Guide Complet pour Débutants


[OK] QU'EST-CE QUE L'ARCHITECTURE TROIS TIERS ?

L'architecture trois tiers (Three-Tier Architecture) est un modèle de conception logicielle
qui sépare une application en trois couches distinctes et indépendantes :

1. TIER 1 - PRÉSENTATION (Frontend/Client)
   - Interface utilisateur (UI)
   - Affichage des données
   - Interaction avec l'utilisateur
   - Exemple: React, Vue.js, Angular

2. TIER 2 - LOGIQUE MÉTIER (Backend/Application)
   - Traitement des requêtes
   - Règles métier
   - Logique applicative
   - Exemple: Flask, Django, Express.js

3. TIER 3 - DONNÉES (Database)
   - Stockage des données
   - Gestion de la persistance
   - Requêtes SQL
   - Exemple: PostgreSQL, MySQL, MongoDB

┌─────────────────────┐
│   PRÉSENTATION      │  <- Utilisateur interagit ici
│    (React)          │
└──────────┬──────────┘
           │ HTTP/REST
           v
┌─────────────────────┐
│  LOGIQUE MÉTIER     │  <- Traitement et règles
│     (Flask)         │
└──────────┬──────────┘
           │ SQL
           v
┌─────────────────────┐
│     DONNÉES         │  <- Stockage persistant
│   (PostgreSQL)      │
└─────────────────────┘


[OK] POURQUOI UTILISER CETTE ARCHITECTURE ?

# Avantages
[OK] Séparation des responsabilités (Separation of Concerns)
[OK] Maintenabilité - Chaque couche peut être modifiée indépendamment
[OK] Scalabilité - Chaque tier peut être déployé sur des serveurs différents
[OK] Réutilisabilité - L'API peut servir plusieurs frontends (web, mobile, desktop)
[OK] Sécurité - La base de données n'est jamais exposée directement
[OK] Testabilité - Chaque couche peut être testée séparément
[OK] Développement parallèle - Équipes frontend et backend travaillent indépendamment

# Inconvénients
[X] Complexité accrue pour petits projets
[X] Latence réseau entre les tiers
[X] Plus de code à maintenir


[OK] STRUCTURE PROJET COMPLÈTE (FLASK + REACT)

myproject/
├── backend/                        # TIER 2 - Logique Métier
│   ├── app/
│   │   ├── __init__.py            # Initialisation Flask
│   │   ├── models.py              # Modèles de données (ORM)
│   │   ├── routes/                # Contrôleurs (endpoints API)
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   ├── users.py
│   │   │   └── products.py
│   │   ├── services/              # Logique métier
│   │   │   ├── __init__.py
│   │   │   ├── auth_service.py
│   │   │   ├── user_service.py
│   │   │   └── product_service.py
│   │   ├── database.py            # Configuration DB
│   │   ├── config.py              # Configuration app
│   │   └── utils.py               # Utilitaires
│   ├── migrations/                # Migrations DB
│   ├── tests/
│   │   ├── test_models.py
│   │   ├── test_routes.py
│   │   └── test_services.py
│   ├── .env                       # Variables d'environnement
│   ├── .env.example
│   ├── requirements.txt
│   └── run.py                     # Point d'entrée
│
├── frontend/                       # TIER 1 - Présentation
│   ├── public/
│   │   ├── index.html
│   │   └── favicon.ico
│   ├── src/
│   │   ├── components/            # Composants UI
│   │   │   ├── Auth/
│   │   │   │   ├── Login.jsx
│   │   │   │   └── Register.jsx
│   │   │   ├── Users/
│   │   │   │   ├── UserList.jsx
│   │   │   │   └── UserDetail.jsx
│   │   │   └── Products/
│   │   │       ├── ProductList.jsx
│   │   │       └── ProductForm.jsx
│   │   ├── services/              # Appels API
│   │   │   ├── api.js
│   │   │   ├── authService.js
│   │   │   ├── userService.js
│   │   │   └── productService.js
│   │   ├── hooks/                 # React Hooks personnalisés
│   │   │   └── useAuth.js
│   │   ├── context/               # Context API (state global)
│   │   │   └── AuthContext.jsx
│   │   ├── utils/
│   │   │   └── helpers.js
│   │   ├── App.jsx
│   │   ├── index.js
│   │   └── index.css
│   ├── .env
│   ├── .env.example
│   ├── package.json
│   └── package-lock.json
│
├── database/                       # TIER 3 - Scripts DB
│   ├── init.sql                   # Initialisation DB
│   ├── seed.sql                   # Données de test
│   └── backup/
│
├── docker-compose.yml             # Orchestration containers
├── .gitignore
└── README.md


[OK] TIER 3 - COUCHE DONNÉES (DATABASE)


# === CONFIGURATION BASE DE DONNÉES ===

# PostgreSQL avec Docker
docker run --name postgres-db \
  -e POSTGRES_USER=myuser \
  -e POSTGRES_PASSWORD=mypassword \
  -e POSTGRES_DB=myapp_db \
  -p 5432:5432 \
  -d postgres:15

# MySQL avec Docker
docker run --name mysql-db \
  -e MYSQL_ROOT_PASSWORD=rootpassword \
  -e MYSQL_DATABASE=myapp_db \
  -e MYSQL_USER=myuser \
  -e MYSQL_PASSWORD=mypassword \
  -p 3306:3306 \
  -d mysql:8

# SQLite (pas de serveur nécessaire)
# Fichier local: database.db


# === SCHÉMA BASE DE DONNÉES ===

-- database/init.sql

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    price DECIMAL(10, 2) NOT NULL,
    stock INTEGER DEFAULT 0,
    created_by INTEGER REFERENCES users(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_items (
    id SERIAL PRIMARY KEY,
    order_id INTEGER REFERENCES orders(id),
    product_id INTEGER REFERENCES products(id),
    quantity INTEGER NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_products_name ON products(name);
CREATE INDEX idx_orders_user ON orders(user_id);


# === DONNÉES DE TEST ===

-- database/seed.sql

INSERT INTO users (username, email, password_hash) VALUES
('admin', 'admin@example.com', '$2b$12$hashed_password_here'),
('john_doe', 'john@example.com', '$2b$12$hashed_password_here'),
('jane_smith', 'jane@example.com', '$2b$12$hashed_password_here');

INSERT INTO products (name, description, price, stock, created_by) VALUES
('Laptop', 'High-performance laptop', 1299.99, 15, 1),
('Mouse', 'Wireless mouse', 29.99, 50, 1),
('Keyboard', 'Mechanical keyboard', 89.99, 30, 1);


[OK] TIER 2 - COUCHE LOGIQUE MÉTIER (BACKEND - FLASK)


# === INSTALLATION BACKEND ===

# Créer environnement virtuel
cd backend
python -m venv .venv

# Activer
source .venv/bin/activate          # Linux/Mac
.venv\Scripts\activate             # Windows

# Installer dépendances
pip install flask flask-sqlalchemy flask-cors flask-jwt-extended \
            python-dotenv psycopg2-binary bcrypt


# === requirements.txt ===

Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-CORS==4.0.0
Flask-JWT-Extended==4.6.0
Flask-Migrate==4.0.5
python-dotenv==1.0.0
psycopg2-binary==2.9.9
bcrypt==4.1.2
marshmallow==3.20.1


# === CONFIGURATION (.env) ===

# backend/.env

# Database
DATABASE_URL=postgresql://myuser:mypassword@localhost:5432/myapp_db

# Flask
FLASK_APP=run.py
FLASK_ENV=development
SECRET_KEY=your-secret-key-change-in-production

# JWT
JWT_SECRET_KEY=your-jwt-secret-key-change-in-production
JWT_ACCESS_TOKEN_EXPIRES=3600

# CORS
CORS_ORIGINS=http://localhost:3000


# === CONFIGURATION APP ===

# backend/app/config.py

import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'dev-jwt-key')
    JWT_ACCESS_TOKEN_EXPIRES = int(os.getenv('JWT_ACCESS_TOKEN_EXPIRES', 3600))

class DevelopmentConfig(Config):
    DEBUG = True
    TESTING = False

class ProductionConfig(Config):
    DEBUG = False
    TESTING = False

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}


# === MODÈLES (ORM) ===

# backend/app/models.py

from datetime import datetime
from app.database import db

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    # Relations
    products = db.relationship('Product', backref='creator', lazy=True)
    orders = db.relationship('Order', backref='user', lazy=True)
    
    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.isoformat()
        }

class Product(db.Model):
    __tablename__ = 'products'
    
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    price = db.Column(db.Numeric(10, 2), nullable=False)
    stock = db.Column(db.Integer, default=0)
    created_by = db.Column(db.Integer, db.ForeignKey('users.id'))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'name': self.name,
            'description': self.description,
            'price': float(self.price),
            'stock': self.stock,
            'created_by': self.created_by,
            'created_at': self.created_at.isoformat()
        }

class Order(db.Model):
    __tablename__ = 'orders'
    
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    total_amount = db.Column(db.Numeric(10, 2), nullable=False)
    status = db.Column(db.String(20), default='pending')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Relations
    items = db.relationship('OrderItem', backref='order', lazy=True, cascade='all, delete-orphan')
    
    def to_dict(self):
        return {
            'id': self.id,
            'user_id': self.user_id,
            'total_amount': float(self.total_amount),
            'status': self.status,
            'items': [item.to_dict() for item in self.items],
            'created_at': self.created_at.isoformat()
        }

class OrderItem(db.Model):
    __tablename__ = 'order_items'
    
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
    product_id = db.Column(db.Integer, db.ForeignKey('products.id'), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    unit_price = db.Column(db.Numeric(10, 2), nullable=False)
    
    # Relations
    product = db.relationship('Product')
    
    def to_dict(self):
        return {
            'id': self.id,
            'product_id': self.product_id,
            'product_name': self.product.name if self.product else None,
            'quantity': self.quantity,
            'unit_price': float(self.unit_price),
            'subtotal': float(self.quantity * self.unit_price)
        }


# === BASE DE DONNÉES ===

# backend/app/database.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def init_db(app):
    """Initialise la base de données"""
    db.init_app(app)
    
    with app.app_context():
        db.create_all()


# === SERVICES (LOGIQUE MÉTIER) ===

# backend/app/services/user_service.py

import bcrypt
from app.models import User
from app.database import db

class UserService:
    
    @staticmethod
    def create_user(username, email, password):
        """Crée un nouvel utilisateur"""
        # Vérifier si l'utilisateur existe
        if User.query.filter_by(email=email).first():
            raise ValueError("Email déjà utilisé")
        
        if User.query.filter_by(username=username).first():
            raise ValueError("Nom d'utilisateur déjà pris")
        
        # Hasher le mot de passe
        password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
        
        # Créer l'utilisateur
        user = User(
            username=username,
            email=email,
            password_hash=password_hash.decode('utf-8')
        )
        
        db.session.add(user)
        db.session.commit()
        
        return user
    
    @staticmethod
    def authenticate(email, password):
        """Authentifie un utilisateur"""
        user = User.query.filter_by(email=email).first()
        
        if not user:
            return None
        
        if bcrypt.checkpw(password.encode('utf-8'), user.password_hash.encode('utf-8')):
            return user
        
        return None
    
    @staticmethod
    def get_user_by_id(user_id):
        """Récupère un utilisateur par ID"""
        return User.query.get(user_id)
    
    @staticmethod
    def get_all_users():
        """Récupère tous les utilisateurs"""
        return User.query.all()
    
    @staticmethod
    def update_user(user_id, **kwargs):
        """Met à jour un utilisateur"""
        user = User.query.get(user_id)
        
        if not user:
            raise ValueError("Utilisateur non trouvé")
        
        for key, value in kwargs.items():
            if hasattr(user, key) and key != 'password_hash':
                setattr(user, key, value)
        
        db.session.commit()
        return user
    
    @staticmethod
    def delete_user(user_id):
        """Supprime un utilisateur"""
        user = User.query.get(user_id)
        
        if not user:
            raise ValueError("Utilisateur non trouvé")
        
        db.session.delete(user)
        db.session.commit()


# backend/app/services/product_service.py

from app.models import Product
from app.database import db

class ProductService:
    
    @staticmethod
    def create_product(name, description, price, stock, created_by):
        """Crée un nouveau produit"""
        product = Product(
            name=name,
            description=description,
            price=price,
            stock=stock,
            created_by=created_by
        )
        
        db.session.add(product)
        db.session.commit()
        
        return product
    
    @staticmethod
    def get_product_by_id(product_id):
        """Récupère un produit par ID"""
        return Product.query.get(product_id)
    
    @staticmethod
    def get_all_products(page=1, per_page=10):
        """Récupère tous les produits avec pagination"""
        return Product.query.paginate(page=page, per_page=per_page, error_out=False)
    
    @staticmethod
    def search_products(query):
        """Recherche des produits par nom"""
        return Product.query.filter(Product.name.ilike(f'%{query}%')).all()
    
    @staticmethod
    def update_product(product_id, **kwargs):
        """Met à jour un produit"""
        product = Product.query.get(product_id)
        
        if not product:
            raise ValueError("Produit non trouvé")
        
        for key, value in kwargs.items():
            if hasattr(product, key):
                setattr(product, key, value)
        
        db.session.commit()
        return product
    
    @staticmethod
    def delete_product(product_id):
        """Supprime un produit"""
        product = Product.query.get(product_id)
        
        if not product:
            raise ValueError("Produit non trouvé")
        
        db.session.delete(product)
        db.session.commit()
    
    @staticmethod
    def update_stock(product_id, quantity):
        """Met à jour le stock d'un produit"""
        product = Product.query.get(product_id)
        
        if not product:
            raise ValueError("Produit non trouvé")
        
        if product.stock + quantity < 0:
            raise ValueError("Stock insuffisant")
        
        product.stock += quantity
        db.session.commit()
        
        return product


# backend/app/services/order_service.py

from app.models import Order, OrderItem, Product
from app.database import db
from decimal import Decimal

class OrderService:
    
    @staticmethod
    def create_order(user_id, items):
        """
        Crée une nouvelle commande
        items: [{'product_id': 1, 'quantity': 2}, ...]
        """
        total_amount = Decimal('0.00')
        order_items = []
        
        # Vérifier disponibilité et calculer total
        for item_data in items:
            product = Product.query.get(item_data['product_id'])
            
            if not product:
                raise ValueError(f"Produit {item_data['product_id']} non trouvé")
            
            if product.stock < item_data['quantity']:
                raise ValueError(f"Stock insuffisant pour {product.name}")
            
            subtotal = product.price * item_data['quantity']
            total_amount += subtotal
            
            order_items.append({
                'product': product,
                'quantity': item_data['quantity'],
                'unit_price': product.price
            })
        
        # Créer la commande
        order = Order(
            user_id=user_id,
            total_amount=total_amount,
            status='pending'
        )
        
        db.session.add(order)
        db.session.flush()  # Pour obtenir l'ID de la commande
        
        # Créer les items et mettre à jour le stock
        for item_data in order_items:
            order_item = OrderItem(
                order_id=order.id,
                product_id=item_data['product'].id,
                quantity=item_data['quantity'],
                unit_price=item_data['unit_price']
            )
            db.session.add(order_item)
            
            # Réduire le stock
            item_data['product'].stock -= item_data['quantity']
        
        db.session.commit()
        
        return order
    
    @staticmethod
    def get_order_by_id(order_id):
        """Récupère une commande par ID"""
        return Order.query.get(order_id)
    
    @staticmethod
    def get_user_orders(user_id):
        """Récupère toutes les commandes d'un utilisateur"""
        return Order.query.filter_by(user_id=user_id).order_by(Order.created_at.desc()).all()
    
    @staticmethod
    def update_order_status(order_id, status):
        """Met à jour le statut d'une commande"""
        valid_statuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
        
        if status not in valid_statuses:
            raise ValueError(f"Statut invalide. Doit être parmi: {valid_statuses}")
        
        order = Order.query.get(order_id)
        
        if not order:
            raise ValueError("Commande non trouvée")
        
        order.status = status
        db.session.commit()
        
        return order
    
    @staticmethod
    def cancel_order(order_id):
        """Annule une commande et restaure le stock"""
        order = Order.query.get(order_id)
        
        if not order:
            raise ValueError("Commande non trouvée")
        
        if order.status == 'cancelled':
            raise ValueError("Commande déjà annulée")
        
        if order.status in ['shipped', 'delivered']:
            raise ValueError("Impossible d'annuler une commande expédiée ou livrée")
        
        # Restaurer le stock
        for item in order.items:
            product = Product.query.get(item.product_id)
            if product:
                product.stock += item.quantity
        
        order.status = 'cancelled'
        db.session.commit()
        
        return order


# === ROUTES (API ENDPOINTS) ===

# backend/app/routes/auth.py

from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from app.services.user_service import UserService

auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')

@auth_bp.route('/register', methods=['POST'])
def register():
    """Inscription d'un nouvel utilisateur"""
    data = request.get_json()
    
    try:
        user = UserService.create_user(
            username=data['username'],
            email=data['email'],
            password=data['password']
        )
        
        return jsonify({
            'message': 'Utilisateur créé avec succès',
            'user': user.to_dict()
        }), 201
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except KeyError as e:
        return jsonify({'error': f'Champ manquant: {str(e)}'}), 400

@auth_bp.route('/login', methods=['POST'])
def login():
    """Connexion utilisateur"""
    data = request.get_json()
    
    try:
        user = UserService.authenticate(
            email=data['email'],
            password=data['password']
        )
        
        if not user:
            return jsonify({'error': 'Email ou mot de passe incorrect'}), 401
        
        # Créer token JWT
        access_token = create_access_token(identity=user.id)
        
        return jsonify({
            'message': 'Connexion réussie',
            'access_token': access_token,
            'user': user.to_dict()
        }), 200
    
    except KeyError as e:
        return jsonify({'error': f'Champ manquant: {str(e)}'}), 400

@auth_bp.route('/me', methods=['GET'])
@jwt_required()
def get_current_user():
    """Récupère l'utilisateur connecté"""
    user_id = get_jwt_identity()
    user = UserService.get_user_by_id(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    return jsonify(user.to_dict()), 200


# backend/app/routes/users.py

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.services.user_service import UserService

users_bp = Blueprint('users', __name__, url_prefix='/api/users')

@users_bp.route('', methods=['GET'])
@jwt_required()
def get_users():
    """Récupère tous les utilisateurs"""
    users = UserService.get_all_users()
    return jsonify([user.to_dict() for user in users]), 200

@users_bp.route('/<int:user_id>', methods=['GET'])
@jwt_required()
def get_user(user_id):
    """Récupère un utilisateur par ID"""
    user = UserService.get_user_by_id(user_id)
    
    if not user:
        return jsonify({'error': 'Utilisateur non trouvé'}), 404
    
    return jsonify(user.to_dict()), 200

@users_bp.route('/<int:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
    """Met à jour un utilisateur"""
    current_user_id = get_jwt_identity()
    
    # Vérifier que l'utilisateur met à jour son propre profil
    if current_user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    data = request.get_json()
    
    try:
        user = UserService.update_user(user_id, **data)
        return jsonify(user.to_dict()), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 404

@users_bp.route('/<int:user_id>', methods=['DELETE'])
@jwt_required()
def delete_user(user_id):
    """Supprime un utilisateur"""
    current_user_id = get_jwt_identity()
    
    # Vérifier que l'utilisateur supprime son propre compte
    if current_user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    try:
        UserService.delete_user(user_id)
        return jsonify({'message': 'Utilisateur supprimé'}), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 404


# backend/app/routes/products.py

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.services.product_service import ProductService

products_bp = Blueprint('products', __name__, url_prefix='/api/products')

@products_bp.route('', methods=['GET'])
def get_products():
    """Récupère tous les produits avec pagination"""
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    pagination = ProductService.get_all_products(page=page, per_page=per_page)
    
    return jsonify({
        'products': [product.to_dict() for product in pagination.items],
        'total': pagination.total,
        'pages': pagination.pages,
        'current_page': pagination.page
    }), 200

@products_bp.route('/search', methods=['GET'])
def search_products():
    """Recherche des produits"""
    query = request.args.get('q', '')
    products = ProductService.search_products(query)
    
    return jsonify([product.to_dict() for product in products]), 200

@products_bp.route('/<int:product_id>', methods=['GET'])
def get_product(product_id):
    """Récupère un produit par ID"""
    product = ProductService.get_product_by_id(product_id)
    
    if not product:
        return jsonify({'error': 'Produit non trouvé'}), 404
    
    return jsonify(product.to_dict()), 200

@products_bp.route('', methods=['POST'])
@jwt_required()
def create_product():
    """Crée un nouveau produit"""
    user_id = get_jwt_identity()
    data = request.get_json()
    
    try:
        product = ProductService.create_product(
            name=data['name'],
            description=data.get('description'),
            price=data['price'],
            stock=data.get('stock', 0),
            created_by=user_id
        )
        
        return jsonify(product.to_dict()), 201
    
    except KeyError as e:
        return jsonify({'error': f'Champ manquant: {str(e)}'}), 400

@products_bp.route('/<int:product_id>', methods=['PUT'])
@jwt_required()
def update_product(product_id):
    """Met à jour un produit"""
    data = request.get_json()
    
    try:
        product = ProductService.update_product(product_id, **data)
        return jsonify(product.to_dict()), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 404

@products_bp.route('/<int:product_id>', methods=['DELETE'])
@jwt_required()
def delete_product(product_id):
    """Supprime un produit"""
    try:
        ProductService.delete_product(product_id)
        return jsonify({'message': 'Produit supprimé'}), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 404


# backend/app/routes/orders.py

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.services.order_service import OrderService

orders_bp = Blueprint('orders', __name__, url_prefix='/api/orders')

@orders_bp.route('', methods=['POST'])
@jwt_required()
def create_order():
    """Crée une nouvelle commande"""
    user_id = get_jwt_identity()
    data = request.get_json()
    
    try:
        order = OrderService.create_order(
            user_id=user_id,
            items=data['items']
        )
        
        return jsonify(order.to_dict()), 201
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except KeyError as e:
        return jsonify({'error': f'Champ manquant: {str(e)}'}), 400

@orders_bp.route('', methods=['GET'])
@jwt_required()
def get_user_orders():
    """Récupère les commandes de l'utilisateur connecté"""
    user_id = get_jwt_identity()
    orders = OrderService.get_user_orders(user_id)
    
    return jsonify([order.to_dict() for order in orders]), 200

@orders_bp.route('/<int:order_id>', methods=['GET'])
@jwt_required()
def get_order(order_id):
    """Récupère une commande par ID"""
    user_id = get_jwt_identity()
    order = OrderService.get_order_by_id(order_id)
    
    if not order:
        return jsonify({'error': 'Commande non trouvée'}), 404
    
    # Vérifier que l'utilisateur possède cette commande
    if order.user_id != user_id:
        return jsonify({'error': 'Non autorisé'}), 403
    
    return jsonify(order.to_dict()), 200

@orders_bp.route('/<int:order_id>/status', methods=['PUT'])
@jwt_required()
def update_order_status(order_id):
    """Met à jour le statut d'une commande"""
    data = request.get_json()
    
    try:
        order = OrderService.update_order_status(order_id, data['status'])
        return jsonify(order.to_dict()), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 400
    except KeyError as e:
        return jsonify({'error': f'Champ manquant: {str(e)}'}), 400

@orders_bp.route('/<int:order_id>/cancel', methods=['POST'])
@jwt_required()
def cancel_order(order_id):
    """Annule une commande"""
    user_id = get_jwt_identity()
    
    try:
        order = OrderService.get_order_by_id(order_id)
        
        if not order:
            return jsonify({'error': 'Commande non trouvée'}), 404
        
        if order.user_id != user_id:
            return jsonify({'error': 'Non autorisé'}), 403
        
        order = OrderService.cancel_order(order_id)
        return jsonify(order.to_dict()), 200
    
    except ValueError as e:
        return jsonify({'error': str(e)}), 400


# === INITIALISATION APP ===

# backend/app/__init__.py

from flask import Flask
from flask_cors import CORS
from flask_jwt_extended import JWTManager
from app.config import config
from app.database import db, init_db

def create_app(config_name='default'):
    """Factory pour créer l'application Flask"""
    app = Flask(__name__)
    
    # Configuration
    app.config.from_object(config[config_name])
    
    # CORS
    CORS(app, origins=app.config.get('CORS_ORIGINS', '*').split(','))
    
    # JWT
    jwt = JWTManager(app)
    
    # Base de données
    init_db(app)
    
    # Enregistrer les blueprints (routes)
    from app.routes.auth import auth_bp
    from app.routes.users import users_bp
    from app.routes.products import products_bp
    from app.routes.orders import orders_bp
    
    app.register_blueprint(auth_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(products_bp)
    app.register_blueprint(orders_bp)
    
    # Route de santé
    @app.route('/api/health')
    def health():
        return {'status': 'ok'}, 200
    
    return app


# === POINT D'ENTRÉE ===

# backend/run.py

from app import create_app
import os

app = create_app(os.getenv('FLASK_ENV', 'development'))

if __name__ == '__main__':
    app.run(
        host='0.0.0.0',
        port=5000,
        debug=True
    )


# === LANCER LE BACKEND ===

# Activer environnement virtuel
source .venv/bin/activate  # Linux/Mac
.venv\Scripts\activate     # Windows

# Variables d'environnement
export FLASK_APP=run.py
export FLASK_ENV=development

# Lancer l'application
python run.py

# Ou avec flask run
flask run --host=0.0.0.0 --port=5000

# En production avec Gunicorn
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5000 run:app


[OK] TIER 1 - COUCHE PRÉSENTATION (FRONTEND - REACT)


# === CRÉATION PROJET REACT ===

# Créer projet avec Vite (recommandé - plus rapide)
npm create vite@latest frontend -- --template react
cd frontend
npm install

# Ou avec Create React App
npx create-react-app frontend
cd frontend

# Installer dépendances
npm install axios react-router-dom


# === STRUCTURE PACKAGE.JSON ===

{
  "name": "frontend",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.20.0",
    "axios": "^1.6.2"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.2.1",
    "vite": "^5.0.8"
  }
}


# === CONFIGURATION ENVIRONNEMENT ===

# frontend/.env

VITE_API_BASE_URL=http://localhost:5000/api


# === CONFIGURATION AXIOS (API CLIENT) ===

// frontend/src/services/api.js

import axios from 'axios';

// Configuration de base
const api = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api',
  headers: {
    'Content-Type': 'application/json'
  }
});

// Intercepteur pour ajouter le token JWT
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Intercepteur pour gérer les erreurs
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token expiré ou invalide
      localStorage.removeItem('access_token');
      localStorage.removeItem('user');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;


# === SERVICES API ===

// frontend/src/services/authService.js

import api from './api';

const authService = {
  // Inscription
  register: async (username, email, password) => {
    const response = await api.post('/auth/register', {
      username,
      email,
      password
    });
    return response.data;
  },

  // Connexion
  login: async (email, password) => {
    const response = await api.post('/auth/login', {
      email,
      password
    });
    
    if (response.data.access_token) {
      localStorage.setItem('access_token', response.data.access_token);
      localStorage.setItem('user', JSON.stringify(response.data.user));
    }
    
    return response.data;
  },

  // Déconnexion
  logout: () => {
    localStorage.removeItem('access_token');
    localStorage.removeItem('user');
  },

  // Utilisateur actuel
  getCurrentUser: async () => {
    const response = await api.get('/auth/me');
    return response.data;
  },

  // Vérifier si connecté
  isAuthenticated: () => {
    return !!localStorage.getItem('access_token');
  },

  // Récupérer utilisateur du localStorage
  getUser: () => {
    const user = localStorage.getItem('user');
    return user ? JSON.parse(user) : null;
  }
};

export default authService;


// frontend/src/services/userService.js

import api from './api';

const userService = {
  // Récupérer tous les utilisateurs
  getUsers: async () => {
    const response = await api.get('/users');
    return response.data;
  },

  // Récupérer un utilisateur
  getUser: async (userId) => {
    const response = await api.get(`/users/${userId}`);
    return response.data;
  },

  // Mettre à jour un utilisateur
  updateUser: async (userId, data) => {
    const response = await api.put(`/users/${userId}`, data);
    return response.data;
  },

  // Supprimer un utilisateur
  deleteUser: async (userId) => {
    const response = await api.delete(`/users/${userId}`);
    return response.data;
  }
};

export default userService;


// frontend/src/services/productService.js

import api from './api';

const productService = {
  // Récupérer tous les produits
  getProducts: async (page = 1, perPage = 10) => {
    const response = await api.get('/products', {
      params: { page, per_page: perPage }
    });
    return response.data;
  },

  // Rechercher des produits
  searchProducts: async (query) => {
    const response = await api.get('/products/search', {
      params: { q: query }
    });
    return response.data;
  },

  // Récupérer un produit
  getProduct: async (productId) => {
    const response = await api.get(`/products/${productId}`);
    return response.data;
  },

  // Créer un produit
  createProduct: async (data) => {
    const response = await api.post('/products', data);
    return response.data;
  },

  // Mettre à jour un produit
  updateProduct: async (productId, data) => {
    const response = await api.put(`/products/${productId}`, data);
    return response.data;
  },

  // Supprimer un produit
  deleteProduct: async (productId) => {
    const response = await api.delete(`/products/${productId}`);
    return response.data;
  }
};

export default productService;


// frontend/src/services/orderService.js

import api from './api';

const orderService = {
  // Créer une commande
  createOrder: async (items) => {
    const response = await api.post('/orders', { items });
    return response.data;
  },

  // Récupérer mes commandes
  getMyOrders: async () => {
    const response = await api.get('/orders');
    return response.data;
  },

  // Récupérer une commande
  getOrder: async (orderId) => {
    const response = await api.get(`/orders/${orderId}`);
    return response.data;
  },

  // Mettre à jour statut
  updateOrderStatus: async (orderId, status) => {
    const response = await api.put(`/orders/${orderId}/status`, { status });
    return response.data;
  },

  // Annuler une commande
  cancelOrder: async (orderId) => {
    const response = await api.post(`/orders/${orderId}/cancel`);
    return response.data;
  }
};

export default orderService;


# === CONTEXT API (GESTION STATE GLOBAL) ===

// frontend/src/context/AuthContext.jsx

import React, { createContext, useState, useContext, useEffect } from 'react';
import authService from '../services/authService';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Charger l'utilisateur au montage
    const storedUser = authService.getUser();
    if (storedUser) {
      setUser(storedUser);
    }
    setLoading(false);
  }, []);

  const login = async (email, password) => {
    const data = await authService.login(email, password);
    setUser(data.user);
    return data;
  };

  const register = async (username, email, password) => {
    const data = await authService.register(username, email, password);
    return data;
  };

  const logout = () => {
    authService.logout();
    setUser(null);
  };

  const value = {
    user,
    login,
    register,
    logout,
    isAuthenticated: !!user,
    loading
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};


# === COMPOSANTS REACT ===

// frontend/src/components/Auth/Login.jsx

import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';

const Login = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      await login(email, password);
      navigate('/products');
    } catch (err) {
      setError(err.response?.data?.error || 'Erreur de connexion');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="login-container">
      <h2>Connexion</h2>
      
      {error && <div className="error-message">{error}</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            disabled={loading}
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">Mot de passe</label>
          <input
            type="password"
            id="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            disabled={loading}
          />
        </div>

        <button type="submit" disabled={loading}>
          {loading ? 'Connexion...' : 'Se connecter'}
        </button>
      </form>

      <p>
        Pas de compte ? <Link to="/register">S'inscrire</Link>
      </p>
    </div>
  );
};

export default Login;


// frontend/src/components/Auth/Register.jsx

import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';

const Register = () => {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { register } = useAuth();
  const navigate = useNavigate();

  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');

    if (formData.password !== formData.confirmPassword) {
      setError('Les mots de passe ne correspondent pas');
      return;
    }

    setLoading(true);

    try {
      await register(formData.username, formData.email, formData.password);
      navigate('/login');
    } catch (err) {
      setError(err.response?.data?.error || 'Erreur d\'inscription');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="register-container">
      <h2>Inscription</h2>
      
      {error && <div className="error-message">{error}</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="username">Nom d'utilisateur</label>
          <input
            type="text"
            id="username"
            name="username"
            value={formData.username}
            onChange={handleChange}
            required
            disabled={loading}
          />
        </div>

        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input
            type="email"
            id="email"
            name="email"
            value={formData.email}
            onChange={handleChange}
            required
            disabled={loading}
          />
        </div>

        <div className="form-group">
          <label htmlFor="password">Mot de passe</label>
          <input
            type="password"
            id="password"
            name="password"
            value={formData.password}
            onChange={handleChange}
            required
            disabled={loading}
          />
        </div>

        <div className="form-group">
          <label htmlFor="confirmPassword">Confirmer le mot de passe</label>
          <input
            type="password"
            id="confirmPassword"
            name="confirmPassword"
            value={formData.confirmPassword}
            onChange={handleChange}
            required
            disabled={loading}
          />
        </div>

        <button type="submit" disabled={loading}>
          {loading ? 'Inscription...' : 'S\'inscrire'}
        </button>
      </form>

      <p>
        Déjà un compte ? <Link to="/login">Se connecter</Link>
      </p>
    </div>
  );
};

export default Register;


// frontend/src/components/Products/ProductList.jsx

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import productService from '../../services/productService';

const ProductList = () => {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);
  const [searchQuery, setSearchQuery] = useState('');

  useEffect(() => {
    fetchProducts();
  }, [page]);

  const fetchProducts = async () => {
    setLoading(true);
    setError('');

    try {
      const data = await productService.getProducts(page, 10);
      setProducts(data.products);
      setTotalPages(data.pages);
    } catch (err) {
      setError('Erreur lors du chargement des produits');
    } finally {
      setLoading(false);
    }
  };

  const handleSearch = async (e) => {
    e.preventDefault();
    
    if (!searchQuery.trim()) {
      fetchProducts();
      return;
    }

    setLoading(true);
    try {
      const data = await productService.searchProducts(searchQuery);
      setProducts(data);
    } catch (err) {
      setError('Erreur lors de la recherche');
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div>Chargement...</div>;
  if (error) return <div className="error">{error}</div>;

  return (
    <div className="product-list-container">
      <div className="header">
        <h2>Produits</h2>
        <Link to="/products/new" className="btn-primary">
          Nouveau produit
        </Link>
      </div>

      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          placeholder="Rechercher un produit..."
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
        />
        <button type="submit">Rechercher</button>
      </form>

      <div className="product-grid">
        {products.length === 0 ? (
          <p>Aucun produit trouvé</p>
        ) : (
          products.map((product) => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p className="description">{product.description}</p>
              <p className="price">{product.price} €</p>
              <p className="stock">
                Stock: {product.stock > 0 ? product.stock : 'Épuisé'}
              </p>
              <Link to={`/products/${product.id}`} className="btn-secondary">
                Voir détails
              </Link>
            </div>
          ))
        )}
      </div>

      {totalPages > 1 && (
        <div className="pagination">
          <button
            onClick={() => setPage(p => Math.max(1, p - 1))}
            disabled={page === 1}
          >
            Précédent
          </button>
          <span>Page {page} sur {totalPages}</span>
          <button
            onClick={() => setPage(p => Math.min(totalPages, p + 1))}
            disabled={page === totalPages}
          >
            Suivant
          </button>
        </div>
      )}
    </div>
  );
};

export default ProductList;


// frontend/src/components/Products/ProductDetail.jsx

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import productService from '../../services/productService';
import orderService from '../../services/orderService';
import { useAuth } from '../../context/AuthContext';

const ProductDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const { isAuthenticated } = useAuth();
  
  const [product, setProduct] = useState(null);
  const [quantity, setQuantity] = useState(1);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [orderLoading, setOrderLoading] = useState(false);

  useEffect(() => {
    fetchProduct();
  }, [id]);

  const fetchProduct = async () => {
    setLoading(true);
    try {
      const data = await productService.getProduct(id);
      setProduct(data);
    } catch (err) {
      setError('Produit non trouvé');
    } finally {
      setLoading(false);
    }
  };

  const handleAddToCart = async () => {
    if (!isAuthenticated) {
      navigate('/login');
      return;
    }

    setOrderLoading(true);
    try {
      await orderService.createOrder([
        { product_id: product.id, quantity }
      ]);
      alert('Commande créée avec succès !');
      navigate('/orders');
    } catch (err) {
      alert(err.response?.data?.error || 'Erreur lors de la commande');
    } finally {
      setOrderLoading(false);
    }
  };

  if (loading) return <div>Chargement...</div>;
  if (error) return <div className="error">{error}</div>;
  if (!product) return null;

  return (
    <div className="product-detail-container">
      <button onClick={() => navigate('/products')} className="btn-back">
        <- Retour
      </button>

      <div className="product-detail">
        <h1>{product.name}</h1>
        <p className="description">{product.description}</p>
        <p className="price">{product.price} €</p>
        <p className="stock">
          Stock disponible: {product.stock}
        </p>

        {product.stock > 0 && (
          <div className="order-section">
            <div className="quantity-selector">
              <label htmlFor="quantity">Quantité:</label>
              <input
                type="number"
                id="quantity"
                min="1"
                max={product.stock}
                value={quantity}
                onChange={(e) => setQuantity(parseInt(e.target.value))}
              />
            </div>

            <button
              onClick={handleAddToCart}
              disabled={orderLoading}
              className="btn-primary"
            >
              {orderLoading ? 'Commande en cours...' : 'Commander'}
            </button>
          </div>
        )}

        {product.stock === 0 && (
          <p className="out-of-stock">Produit épuisé</p>
        )}
      </div>
    </div>
  );
};

export default ProductDetail;


// frontend/src/components/Products/ProductForm.jsx

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import productService from '../../services/productService';

const ProductForm = () => {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    price: '',
    stock: ''
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      await productService.createProduct({
        ...formData,
        price: parseFloat(formData.price),
        stock: parseInt(formData.stock)
      });
      navigate('/products');
    } catch (err) {
      setError(err.response?.data?.error || 'Erreur lors de la création');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="product-form-container">
      <h2>Nouveau produit</h2>

      {error && <div className="error-message">{error}</div>}

      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label htmlFor="name">Nom du produit</label>
          <input
            type="text"
            id="name"
            name="name"
            value={formData.name}
            onChange={handleChange}
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor="description">Description</label>
          <textarea
            id="description"
            name="description"
            value={formData.description}
            onChange={handleChange}
            rows="4"
          />
        </div>

        <div className="form-group">
          <label htmlFor="price">Prix (€)</label>
          <input
            type="number"
            id="price"
            name="price"
            step="0.01"
            min="0"
            value={formData.price}
            onChange={handleChange}
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor="stock">Stock</label>
          <input
            type="number"
            id="stock"
            name="stock"
            min="0"
            value={formData.stock}
            onChange={handleChange}
            required
          />
        </div>

        <div className="form-actions">
          <button type="button" onClick={() => navigate('/products')}>
            Annuler
          </button>
          <button type="submit" disabled={loading}>
            {loading ? 'Création...' : 'Créer'}
          </button>
        </div>
      </form>
    </div>
  );
};

export default ProductForm;


// frontend/src/components/Orders/OrderList.jsx

import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import orderService from '../../services/orderService';

const OrderList = () => {
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    fetchOrders();
  }, []);

  const fetchOrders = async () => {
    setLoading(true);
    try {
      const data = await orderService.getMyOrders();
      setOrders(data);
    } catch (err) {
      setError('Erreur lors du chargement des commandes');
    } finally {
      setLoading(false);
    }
  };

  const getStatusBadge = (status) => {
    const statusMap = {
      pending: { label: 'En attente', className: 'status-pending' },
      processing: { label: 'En traitement', className: 'status-processing' },
      shipped: { label: 'Expédié', className: 'status-shipped' },
      delivered: { label: 'Livré', className: 'status-delivered' },
      cancelled: { label: 'Annulé', className: 'status-cancelled' }
    };
    
    const statusInfo = statusMap[status] || { label: status, className: '' };
    return <span className={`status-badge ${statusInfo.className}`}>{statusInfo.label}</span>;
  };

  if (loading) return <div>Chargement...</div>;
  if (error) return <div className="error">{error}</div>;

  return (
    <div className="order-list-container">
      <h2>Mes commandes</h2>

      {orders.length === 0 ? (
        <div className="empty-state">
          <p>Vous n'avez pas encore de commandes</p>
          <Link to="/products" className="btn-primary">
            Voir les produits
          </Link>
        </div>
      ) : (
        <div className="orders-list">
          {orders.map((order) => (
            <div key={order.id} className="order-card">
              <div className="order-header">
                <h3>Commande #{order.id}</h3>
                {getStatusBadge(order.status)}
              </div>
              
              <div className="order-info">
                <p>Date: {new Date(order.created_at).toLocaleDateString('fr-FR')}</p>
                <p className="total">Total: {order.total_amount} €</p>
                <p>Articles: {order.items.length}</p>
              </div>

              <div className="order-actions">
                <Link to={`/orders/${order.id}`} className="btn-secondary">
                  Voir détails
                </Link>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default OrderList;


// frontend/src/components/Orders/OrderDetail.jsx

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import orderService from '../../services/orderService';

const OrderDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  
  const [order, setOrder] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  const [cancelLoading, setCancelLoading] = useState(false);

  useEffect(() => {
    fetchOrder();
  }, [id]);

  const fetchOrder = async () => {
    setLoading(true);
    try {
      const data = await orderService.getOrder(id);
      setOrder(data);
    } catch (err) {
      setError('Commande non trouvée');
    } finally {
      setLoading(false);
    }
  };

  const handleCancel = async () => {
    if (!window.confirm('Êtes-vous sûr de vouloir annuler cette commande ?')) {
      return;
    }

    setCancelLoading(true);
    try {
      await orderService.cancelOrder(id);
      fetchOrder(); // Recharger la commande
      alert('Commande annulée avec succès');
    } catch (err) {
      alert(err.response?.data?.error || 'Erreur lors de l\'annulation');
    } finally {
      setCancelLoading(false);
    }
  };

  if (loading) return <div>Chargement...</div>;
  if (error) return <div className="error">{error}</div>;
  if (!order) return null;

  const canCancel = ['pending', 'processing'].includes(order.status);

  return (
    <div className="order-detail-container">
      <button onClick={() => navigate('/orders')} className="btn-back">
        <- Retour
      </button>

      <div className="order-detail">
        <div className="order-header">
          <h1>Commande #{order.id}</h1>
          <span className={`status-badge status-${order.status}`}>
            {order.status}
          </span>
        </div>

        <div className="order-info-section">
          <h3>Informations</h3>
          <p>Date: {new Date(order.created_at).toLocaleDateString('fr-FR')}</p>
          <p className="total">Total: {order.total_amount} €</p>
        </div>

        <div className="order-items-section">
          <h3>Articles</h3>
          <table className="order-items-table">
            <thead>
              <tr>
                <th>Produit</th>
                <th>Prix unitaire</th>
                <th>Quantité</th>
                <th>Sous-total</th>
              </tr>
            </thead>
            <tbody>
              {order.items.map((item) => (
                <tr key={item.id}>
                  <td>{item.product_name}</td>
                  <td>{item.unit_price} €</td>
                  <td>{item.quantity}</td>
                  <td>{item.subtotal} €</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        {canCancel && (
          <div className="order-actions">
            <button
              onClick={handleCancel}
              disabled={cancelLoading}
              className="btn-danger"
            >
              {cancelLoading ? 'Annulation...' : 'Annuler la commande'}
            </button>
          </div>
        )}
      </div>
    </div>
  );
};

export default OrderDetail;


# === ROUTES PROTECTION ===

// frontend/src/components/ProtectedRoute.jsx

import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, loading } = useAuth();

  if (loading) {
    return <div>Chargement...</div>;
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return children;
};

export default ProtectedRoute;


// frontend/src/components/Layout/Navbar.jsx

import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';

const Navbar = () => {
  const { user, isAuthenticated, logout } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate('/login');
  };

  return (
    <nav className="navbar">
      <div className="navbar-brand">
        <Link to="/">MyApp</Link>
      </div>

      <ul className="navbar-menu">
        <li><Link to="/products">Produits</Link></li>
        
        {isAuthenticated ? (
          <>
            <li><Link to="/orders">Mes commandes</Link></li>
            <li className="user-menu">
              <span>Bonjour, {user?.username}</span>
              <button onClick={handleLogout} className="btn-logout">
                Déconnexion
              </button>
            </li>
          </>
        ) : (
          <>
            <li><Link to="/login">Connexion</Link></li>
            <li><Link to="/register">Inscription</Link></li>
          </>
        )}
      </ul>
    </nav>
  );
};

export default Navbar;


# === ROUTING ===

// frontend/src/App.jsx

import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import Navbar from './components/Layout/Navbar';
import ProtectedRoute from './components/ProtectedRoute';

// Auth
import Login from './components/Auth/Login';
import Register from './components/Auth/Register';

// Products
import ProductList from './components/Products/ProductList';
import ProductDetail from './components/Products/ProductDetail';
import ProductForm from './components/Products/ProductForm';

// Orders
import OrderList from './components/Orders/OrderList';
import OrderDetail from './components/Orders/OrderDetail';

function App() {
  return (
    <AuthProvider>
      <Router>
        <div className="app">
          <Navbar />
          
          <main className="main-content">
            <Routes>
              {/* Public routes */}
              <Route path="/" element={<Navigate to="/products" replace />} />
              <Route path="/login" element={<Login />} />
              <Route path="/register" element={<Register />} />
              
              {/* Products */}
              <Route path="/products" element={<ProductList />} />
              <Route path="/products/:id" element={<ProductDetail />} />
              <Route
                path="/products/new"
                element={
                  <ProtectedRoute>
                    <ProductForm />
                  </ProtectedRoute>
                }
              />
              
              {/* Orders - Protected */}
              <Route
                path="/orders"
                element={
                  <ProtectedRoute>
                    <OrderList />
                  </ProtectedRoute>
                }
              />
              <Route
                path="/orders/:id"
                element={
                  <ProtectedRoute>
                    <OrderDetail />
                  </ProtectedRoute>
                }
              />
              
              {/* 404 */}
              <Route path="*" element={<div>Page non trouvée</div>} />
            </Routes>
          </main>
        </div>
      </Router>
    </AuthProvider>
  );
}

export default App;


# === STYLES CSS ===

// frontend/src/index.css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
  background-color: #f5f5f5;
  color: #333;
  line-height: 1.6;
}

.app {
  min-height: 100vh;
}

/* Navbar */
.navbar {
  background-color: #2c3e50;
  color: white;
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.navbar-brand a {
  color: white;
  text-decoration: none;
  font-size: 1.5rem;
  font-weight: bold;
}

.navbar-menu {
  display: flex;
  list-style: none;
  gap: 2rem;
  align-items: center;
}

.navbar-menu a {
  color: white;
  text-decoration: none;
}

.navbar-menu a:hover {
  text-decoration: underline;
}

/* Main content */
.main-content {
  max-width: 1200px;
  margin: 2rem auto;
  padding: 0 2rem;
}

/* Forms */
.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #3498db;
}

/* Buttons */
button,
.btn-primary,
.btn-secondary,
.btn-danger {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
  transition: background-color 0.3s;
}

.btn-primary {
  background-color: #3498db;
  color: white;
}

.btn-primary:hover {
  background-color: #2980b9;
}

.btn-secondary {
  background-color: #95a5a6;
  color: white;
}

.btn-secondary:hover {
  background-color: #7f8c8d;
}

.btn-danger {
  background-color: #e74c3c;
  color: white;
}

.btn-danger:hover {
  background-color: #c0392b;
}

button:disabled {
  background-color: #bdc3c7;
  cursor: not-allowed;
}

/* Products Grid */
.product-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 2rem;
  margin: 2rem 0;
}

.product-card {
  background: white;
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.product-card h3 {
  margin-bottom: 0.5rem;
}

.product-card .price {
  font-size: 1.5rem;
  font-weight: bold;
  color: #27ae60;
  margin: 1rem 0;
}

/* Status badges */
.status-badge {
  padding: 0.25rem 0.75rem;
  border-radius: 12px;
  font-size: 0.875rem;
  font-weight: 500;
}

.status-pending { background-color: #f39c12; color: white; }
.status-processing { background-color: #3498db; color: white; }
.status-shipped { background-color: #9b59b6; color: white; }
.status-delivered { background-color: #27ae60; color: white; }
.status-cancelled { background-color: #95a5a6; color: white; }

/* Error messages */
.error-message {
  background-color: #e74c3c;
  color: white;
  padding: 1rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}

/* Loading */
.loading {
  text-align: center;
  padding: 2rem;
  font-size: 1.2rem;
  color: #7f8c8d;
}


# === LANCER LE FRONTEND ===

cd frontend

# Développement
npm run dev

# Production
npm run build
npm run preview


[OK] COMMUNICATION ENTRE LES TIERS - EXPLICATIONS DÉTAILLÉES


# === COMPRENDRE LE FLUX COMPLET DES DONNÉES ===

Quand vous travaillez avec une architecture trois tiers, les données circulent
entre les couches en suivant des conventions de nommage. Comprendre comment
les mêmes informations sont représentées à chaque niveau est CRUCIAL.

# EXEMPLE CONCRET: Le champ "email"

1. DATABASE (PostgreSQL)
   - Nom de colonne: email
   - Type SQL: VARCHAR(100)
   - Contrainte: UNIQUE, NOT NULL
   
   CREATE TABLE users (
       email VARCHAR(100) UNIQUE NOT NULL
   );

2. BACKEND - Modèle ORM (SQLAlchemy)
   - Attribut Python: email
   - Mappe automatiquement vers la colonne 'email' de la DB
   
   class User(db.Model):
       email = db.Column(db.String(100), unique=True, nullable=False)
       #       ^ attribut Python  ^ colonne SQL

3. BACKEND - Service (Logique métier)
   - Paramètre de fonction: email
   - Utilise l'attribut du modèle
   
   def create_user(username, email, password):
       user = User(email=email)  # email du paramètre -> email du modèle
       #           ^ attribut    ^ valeur

4. BACKEND - Route API (JSON)
   - Clé JSON: "email"
   - Extrait depuis request.get_json()
   
   @auth_bp.route('/register', methods=['POST'])
   def register():
       data = request.get_json()
       email = data['email']  # Lit la clé "email" du JSON
       #       ^ dict Python   ^ clé JSON

5. FRONTEND - Service API (Axios)
   - Propriété d'objet JavaScript: email
   - Envoyé dans le body de la requête
   
   register: async (username, email, password) => {
       await api.post('/auth/register', {
           email: email  // Clé JSON: email
       });
   }

6. FRONTEND - Composant React
   - Variable d'état: email
   - Valeur de l'input
   
   const [email, setEmail] = useState('');
   <input value={email} onChange={(e) => setEmail(e.target.value)} />
   //            ^ state     ^ met à jour le state


# === TRAÇABILITÉ COMPLÈTE D'UNE VALEUR ===

Suivons "user@example.com" depuis l'interface jusqu'à la base de données:

┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 1: Utilisateur tape dans l'input                          │
│ ────────────────────────────────────────────                    │
│ React Component:                                                │
│   <input value={email} />                                       │
│   email = "user@example.com"  <- État React                      │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 2: Soumission du formulaire                               │
│ ────────────────────────────────────────────────                │
│ authService.register(username, email, password)                 │
│   v                                                             │
│ api.post('/auth/register', {                                    │
│   username: "john",                                             │
│   email: "user@example.com",  <- Devient clé JSON               │
│   password: "pass123"                                           │
│ })                                                              │
└─────────────────────────────────────────────────────────────────┘
                            v HTTP POST
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 3: Requête HTTP arrive au backend                         │
│ ────────────────────────────────────────────────                │
│ POST /api/auth/register HTTP/1.1                                │
│ Content-Type: application/json                                  │
│                                                                 │
│ {                                                               │
│   "username": "john",                                           │
│   "email": "user@example.com",  <- Corps JSON de la requête     │
│   "password": "pass123"                                         │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 4: Flask parse le JSON                                    │
│ ────────────────────────────────────────────────                │
│ @auth_bp.route('/register', methods=['POST'])                   │
│ def register():                                                 │
│     data = request.get_json()                                   │
│     # data = {                                                  │
│     #   'username': 'john',                                     │
│     #   'email': 'user@example.com',  <- Dictionnaire Python    │
│     #   'password': 'pass123'                                   │
│     # }                                                         │
│                                                                 │
│     email = data['email']  # Extraction de la valeur           │
│     # email = "user@example.com"                               │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 5: Appel du service                                       │
│ ────────────────────────────────────────────────                │
│ user = UserService.create_user(                                 │
│     username=data['username'],                                  │
│     email=data['email'],  <- Passé comme argument               │
│     password=data['password']                                   │
│ )                                                               │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 6: Service crée l'objet User                              │
│ ────────────────────────────────────────────────                │
│ def create_user(username, email, password):                     │
│     # email = "user@example.com" <- Paramètre de fonction       │
│                                                                 │
│     user = User(                                                │
│         username=username,                                      │
│         email=email,  <- Assigné à l'attribut du modèle         │
│         password_hash=hash_password(password)                   │
│     )                                                           │
│     # user.email = "user@example.com"                          │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 7: SQLAlchemy prépare l'INSERT                            │
│ ────────────────────────────────────────────────                │
│ db.session.add(user)                                            │
│ db.session.commit()                                             │
│                                                                 │
│ SQLAlchemy génère automatiquement:                              │
│ INSERT INTO users (username, email, password_hash)              │
│ VALUES ('john', 'user@example.com', '$2b$12...')               │
│                      ^                                          │
│                      └─ Attribut user.email devient valeur SQL  │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 8: PostgreSQL stocke la donnée                            │
│ ────────────────────────────────────────────────                │
│ Table: users                                                    │
│ ┌────┬──────────┬──────────────────┬───────────────┐           │
│ │ id │ username │      email       │ password_hash │           │
│ ├────┼──────────┼──────────────────┼───────────────┤           │
│ │ 1  │ john     │user@example.com  │ $2b$12...     │           │
│ └────┴──────────┴──────────────────┴───────────────┘           │
│                       ^                                         │
│                       └─ Stocké dans la colonne 'email'         │
└─────────────────────────────────────────────────────────────────┘


# === SENS INVERSE: Lecture de données ===

Suivons la lecture de cet email depuis la DB vers le frontend:

┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 1: Requête SQL SELECT                                     │
│ ────────────────────────────────────────────────                │
│ SELECT id, username, email FROM users WHERE email = ?           │
│                                                                 │
│ Résultat PostgreSQL:                                            │
│ (1, 'john', 'user@example.com')                                │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 2: SQLAlchemy mappe vers objet Python                     │
│ ────────────────────────────────────────────────                │
│ user = User.query.filter_by(email='user@example.com').first()  │
│                                                                 │
│ # user devient un objet User:                                   │
│ # user.id = 1                                                   │
│ # user.username = 'john'                                        │
│ # user.email = 'user@example.com'  <- Attribut Python           │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 3: Conversion en dictionnaire (sérialisation)             │
│ ────────────────────────────────────────────────────────────────│
│ def to_dict(self):                                              │
│     return {                                                    │
│         'id': self.id,                                          │
│         'username': self.username,                              │
│         'email': self.email  <- Attribut -> clé dict             │
│     }                                                           │
│                                                                 │
│ user_dict = user.to_dict()                                      │
│ # user_dict = {                                                 │
│ #     'id': 1,                                                  │
│ #     'username': 'john',                                       │
│ #     'email': 'user@example.com'  <- Clé de dictionnaire       │
│ # }                                                             │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 4: Flask sérialise en JSON                                │
│ ────────────────────────────────────────────────────────────────│
│ return jsonify(user.to_dict()), 200                             │
│                                                                 │
│ Réponse HTTP:                                                   │
│ HTTP/1.1 200 OK                                                 │
│ Content-Type: application/json                                  │
│                                                                 │
│ {                                                               │
│   "id": 1,                                                      │
│   "username": "john",                                           │
│   "email": "user@example.com"  <- Clé JSON                      │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘
                            v HTTP Response
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 5: Axios reçoit la réponse                                │
│ ────────────────────────────────────────────────────────────────│
│ const response = await api.get('/auth/me');                     │
│ const data = response.data;                                     │
│                                                                 │
│ // data = {                                                     │
│ //   id: 1,                                                     │
│ //   username: "john",                                          │
│ //   email: "user@example.com"  <- Propriété JavaScript         │
│ // }                                                            │
└─────────────────────────────────────────────────────────────────┘
                            v
┌─────────────────────────────────────────────────────────────────┐
│ ÉTAPE 6: React affiche la donnée                                │
│ ────────────────────────────────────────────────────────────────│
│ const [user, setUser] = useState(null);                         │
│                                                                 │
│ useEffect(() => {                                               │
│   authService.getCurrentUser().then(data => {                   │
│     setUser(data);  // data.email = "user@example.com"         │
│   });                                                           │
│ }, []);                                                         │
│                                                                 │
│ return <div>Email: {user.email}</div>                           │
│                              ^                                  │
│                              └─ Propriété de l'objet user       │
└─────────────────────────────────────────────────────────────────┘


# === CONVENTIONS DE NOMMAGE À CHAQUE NIVEAU ===

┌──────────────┬──────────────────┬───────────────────────────────┐
│   NIVEAU     │   CONVENTION     │         EXEMPLE               │
├──────────────┼──────────────────┼───────────────────────────────┤
│ Database     │ snake_case       │ user_email, created_at        │
│ (PostgreSQL) │ (colonnes SQL)   │ product_name, order_status    │
├──────────────┼──────────────────┼───────────────────────────────┤
│ Backend      │ snake_case       │ user_email, created_at        │
│ (Python)     │ (attributs)      │ product_name, order_status    │
├──────────────┼──────────────────┼───────────────────────────────┤
│ API JSON     │ snake_case OU    │ user_email OU userEmail       │
│              │ camelCase        │ (dépend de la convention)     │
├──────────────┼──────────────────┼───────────────────────────────┤
│ Frontend     │ camelCase        │ userEmail, createdAt          │
│ (JavaScript) │ (variables)      │ productName, orderStatus      │
└──────────────┴──────────────────┴───────────────────────────────┘

# IMPORTANT: Transformation automatique

Si vous utilisez snake_case en Python et camelCase en JavaScript,
vous devrez convertir:

# Backend - Sérialisation (Python -> JSON)
def to_dict(self):
    return {
        'userId': self.user_id,      # snake_case -> camelCase
        'createdAt': self.created_at.isoformat()
    }

# Frontend - Désérialisation (JSON -> JavaScript)
// Pas de conversion nécessaire, JavaScript utilise directement:
console.log(data.userId);
console.log(data.createdAt);

# Frontend -> Backend (JavaScript -> JSON -> Python)
// Frontend envoie camelCase
api.post('/users', {
    firstName: 'John',
    lastName: 'Doe'
});

# Backend reçoit et doit convertir
@users_bp.route('', methods=['POST'])
def create_user():
    data = request.get_json()
    
    # Convertir camelCase -> snake_case
    first_name = data.get('firstName')
    last_name = data.get('lastName')
    
    # Ou utiliser une bibliothèque comme humps
    from humps import decamelize
    data = decamelize(data)  # {'firstName': 'John'} -> {'first_name': 'John'}


# === EXEMPLE COMPLET AVEC PLUSIEURS CHAMPS ===

1. BASE DE DONNÉES
──────────────────
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    product_name VARCHAR(100),      <- snake_case
    unit_price DECIMAL(10,2),       <- snake_case
    stock_quantity INTEGER,         <- snake_case
    created_at TIMESTAMP            <- snake_case
);

2. MODÈLE BACKEND (ORM)
───────────────────────
class Product(db.Model):
    __tablename__ = 'products'
    
    id = db.Column(db.Integer, primary_key=True)
    product_name = db.Column(db.String(100))     # <- Même nom que colonne SQL
    unit_price = db.Column(db.Numeric(10, 2))    # <- Même nom
    stock_quantity = db.Column(db.Integer)       # <- Même nom
    created_at = db.Column(db.DateTime)          # <- Même nom
    
    def to_dict(self):
        return {
            'id': self.id,
            'productName': self.product_name,           # <- snake -> camelCase
            'unitPrice': float(self.unit_price),        # <- snake -> camelCase
            'stockQuantity': self.stock_quantity,       # <- snake -> camelCase
            'createdAt': self.created_at.isoformat()    # <- snake -> camelCase
        }

3. ROUTE API
────────────
@products_bp.route('', methods=['POST'])
def create_product():
    data = request.get_json()
    # data = {
    #     'productName': 'Laptop',      <- Reçu en camelCase
    #     'unitPrice': 1299.99,
    #     'stockQuantity': 10
    # }
    
    # Convertir pour le modèle
    product = Product(
        product_name=data['productName'],       # camelCase -> snake_case
        unit_price=data['unitPrice'],           # camelCase -> snake_case
        stock_quantity=data['stockQuantity']    # camelCase -> snake_case
    )
    
    db.session.add(product)
    db.session.commit()
    
    return jsonify(product.to_dict()), 201
    # Retourne en camelCase pour le frontend

4. SERVICE FRONTEND
───────────────────
const productService = {
    createProduct: async (productData) => {
        // productData = {
        //     productName: 'Laptop',      <- camelCase
        //     unitPrice: 1299.99,
        //     stockQuantity: 10
        // }
        
        const response = await api.post('/products', productData);
        return response.data;
    }
};

5. COMPOSANT REACT
──────────────────
const ProductForm = () => {
    const [productName, setProductName] = useState('');        // <- camelCase
    const [unitPrice, setUnitPrice] = useState('');            // <- camelCase
    const [stockQuantity, setStockQuantity] = useState('');    // <- camelCase
    
    const handleSubmit = async (e) => {
        e.preventDefault();
        
        await productService.createProduct({
            productName,       // <- camelCase
            unitPrice,         // <- camelCase
            stockQuantity      // <- camelCase
        });
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input
                value={productName}
                onChange={(e) => setProductName(e.target.value)}
            />
            {/* ... */}
        </form>
    );
};


# === CORRESPONDANCE DES TYPES DE DONNÉES ===

┌─────────────────┬──────────────────┬──────────────────┬─────────────────┐
│   DATABASE      │    PYTHON        │      JSON        │   JAVASCRIPT    │
│   (PostgreSQL)  │    (Backend)     │   (Transport)    │   (Frontend)    │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ INTEGER         │ int              │ number           │ number          │
│ SERIAL          │ int              │ number           │ number          │
│ BIGINT          │ int              │ number           │ number          │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ VARCHAR(n)      │ str              │ string           │ string          │
│ TEXT            │ str              │ string           │ string          │
│ CHAR(n)         │ str              │ string           │ string          │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ DECIMAL(10,2)   │ Decimal          │ string/number    │ number          │
│ NUMERIC(10,2)   │ Decimal          │ string/number    │ number          │
│ REAL            │ float            │ number           │ number          │
│ DOUBLE          │ float            │ number           │ number          │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ BOOLEAN         │ bool             │ boolean          │ boolean         │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ TIMESTAMP       │ datetime         │ string (ISO)     │ Date/string     │
│ DATE            │ date             │ string (ISO)     │ Date/string     │
│ TIME            │ time             │ string           │ Date/string     │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ JSON            │ dict             │ object           │ object          │
│ JSONB           │ dict             │ object           │ object          │
├─────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ ARRAY           │ list             │ array            │ Array           │
└─────────────────┴──────────────────┴──────────────────┴─────────────────┘

# EXEMPLE CONCRET de chaque type:

# 1. INTEGER
──────────
PostgreSQL:    stock_quantity INTEGER = 42
Python:        product.stock_quantity = 42 (int)
JSON:          {"stockQuantity": 42}
JavaScript:    product.stockQuantity = 42 (number)

# 2. VARCHAR/TEXT
────────────────
PostgreSQL:    product_name VARCHAR(100) = 'Laptop'
Python:        product.product_name = 'Laptop' (str)
JSON:          {"productName": "Laptop"}
JavaScript:    product.productName = "Laptop" (string)

# 3. DECIMAL (ATTENTION: Conversion nécessaire)
───────────────────────────────────────────────
PostgreSQL:    unit_price DECIMAL(10,2) = 1299.99
Python:        product.unit_price = Decimal('1299.99')
JSON:          {"unitPrice": 1299.99}  <- float() nécessaire!
JavaScript:    product.unitPrice = 1299.99 (number)

# Backend: Conversion Decimal -> float
def to_dict(self):
    return {
        'unitPrice': float(self.unit_price)  # <- OBLIGATOIRE!
    }

# 4. BOOLEAN
────────────
PostgreSQL:    is_active BOOLEAN = TRUE
Python:        user.is_active = True (bool)
JSON:          {"isActive": true}
JavaScript:    user.isActive = true (boolean)

# 5. TIMESTAMP (ATTENTION: Sérialisation ISO)
─────────────────────────────────────────────
PostgreSQL:    created_at TIMESTAMP = '2024-01-15 10:30:00'
Python:        product.created_at = datetime(2024, 1, 15, 10, 30, 0)
JSON:          {"createdAt": "2024-01-15T10:30:00"}  <- .isoformat()!
JavaScript:    new Date(product.createdAt)

# Backend: Conversion datetime -> ISO string
def to_dict(self):
    return {
        'createdAt': self.created_at.isoformat()  # <- OBLIGATOIRE!
    }

# Frontend: Conversion ISO string -> Date
const createdDate = new Date(product.createdAt);
console.log(createdDate.toLocaleDateString()); // 15/01/2024

# 6. JSON/JSONB (Données structurées)
─────────────────────────────────────
PostgreSQL:    metadata JSONB = '{"color": "blue", "size": "M"}'
Python:        product.metadata = {'color': 'blue', 'size': 'M'} (dict)
JSON:          {"metadata": {"color": "blue", "size": "M"}}
JavaScript:    product.metadata = {color: "blue", size: "M"} (object)

# 7. ARRAY
──────────
PostgreSQL:    tags TEXT[] = ARRAY['electronics', 'laptop', 'new']
Python:        product.tags = ['electronics', 'laptop', 'new'] (list)
JSON:          {"tags": ["electronics", "laptop", "new"]}
JavaScript:    product.tags = ["electronics", "laptop", "new"] (Array)


# === PIÈGES COURANTS ET SOLUTIONS ===

# PIÈGE 1: Oubli de convertir Decimal en float
──────────────────────────────────────────────

# [X] MAUVAIS - Erreur de sérialisation JSON
def to_dict(self):
    return {
        'price': self.price  # Decimal n'est pas JSON-serializable!
    }

# Erreur: Object of type Decimal is not JSON serializable

# [OK] BON
def to_dict(self):
    return {
        'price': float(self.price)  # Conversion explicite
    }


# PIÈGE 2: Oubli de convertir datetime en string
─────────────────────────────────────────────────

# [X] MAUVAIS
def to_dict(self):
    return {
        'createdAt': self.created_at  # datetime n'est pas JSON-serializable!
    }

# Erreur: Object of type datetime is not JSON serializable

# [OK] BON
def to_dict(self):
    return {
        'createdAt': self.created_at.isoformat()  # "2024-01-15T10:30:00"
    }

# [OK] ENCORE MIEUX - Gérer None
def to_dict(self):
    return {
        'createdAt': self.created_at.isoformat() if self.created_at else None
    }


# PIÈGE 3: Incohérence des noms entre niveaux
──────────────────────────────────────────────

# Situation: Le frontend envoie 'email' mais le backend attend 'user_email'

# Frontend envoie:
api.post('/users', {
    email: 'user@example.com'
});

# Backend s'attend à:
@users_bp.route('', methods=['POST'])
def create_user():
    data = request.get_json()
    email = data['user_email']  # [X] KeyError! 'user_email' n'existe pas

# SOLUTIONS:

# Solution A: Normaliser le nom
# Frontend:
api.post('/users', {
    userEmail: 'user@example.com'  // ou user_email
});

# Backend:
email = data['userEmail']  # ou data['user_email']

# Solution B: Mapper explicitement
@users_bp.route('', methods=['POST'])
def create_user():
    data = request.get_json()
    email = data.get('email') or data.get('user_email')  # Accepte les deux


# PIÈGE 4: Confusion entre None/null/undefined
───────────────────────────────────────────────

PostgreSQL:  NULL
Python:      None
JSON:        null
JavaScript:  null ou undefined

# Backend: Vérifier None
if user.email is None:
    return jsonify({'error': 'Email manquant'}), 400

# Backend: Valeur par défaut pour None
def to_dict(self):
    return {
        'email': self.email or '',  # None -> ''
        'bio': self.bio if self.bio else 'Pas de bio'
    }

# Frontend: Vérifier null/undefined
if (user.email === null || user.email === undefined) {
    console.log('Email manquant');
}

// Ou plus simple:
if (!user.email) {
    console.log('Email manquant');
}


# PIÈGE 5: Relations et chargement des données
───────────────────────────────────────────────

# Modèle avec relation
class Order(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    user = db.relationship('User', backref='orders')  # <- Relation

# [X] MAUVAIS - N+1 queries + erreur sérialisation
def to_dict(self):
    return {
        'id': self.id,
        'user': self.user  # [X] Objet User pas JSON-serializable!
    }

# [OK] BON - Charger explicitement et sérialiser
def to_dict(self):
    return {
        'id': self.id,
        'user': self.user.to_dict() if self.user else None
    }

# [OK] ENCORE MIEUX - Charger avec joinedload
from sqlalchemy.orm import joinedload

orders = Order.query.options(joinedload(Order.user)).all()


# === DEBUGGING: INSPECTER LES DONNÉES À CHAQUE NIVEAU ===

# 1. DATABASE - Vérifier données stockées
──────────────────────────────────────────
psql -U myuser -d myapp_db
SELECT * FROM users WHERE email = 'user@example.com';

# 2. BACKEND - Logger les données reçues
─────────────────────────────────────────
@auth_bp.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    
    print("=== DONNÉES REÇUES ===")
    print(f"Type: {type(data)}")
    print(f"Contenu: {data}")
    print(f"Email: {data.get('email')}")
    print("=" * 30)
    
    # Log avec logger
    app.logger.info(f"Inscription: {data}")

# 3. BACKEND - Inspecter objet créé
────────────────────────────────────
user = User(email=email, ...)

print("=== OBJET USER ===")
print(f"Type: {type(user)}")
print(f"Email: {user.email}")
print(f"Dict: {user.to_dict()}")
print("=" * 30)

# 4. BACKEND - Vérifier JSON envoyé
────────────────────────────────────
response_data = user.to_dict()

print("=== RÉPONSE JSON ===")
print(f"Type: {type(response_data)}")
print(f"Contenu: {response_data}")
print("=" * 30)

return jsonify(response_data), 201

# 5. FRONTEND - Inspecter requête envoyée
─────────────────────────────────────────
// Intercepteur Axios pour logger
api.interceptors.request.use(
  (config) => {
    console.log('=== REQUÊTE ENVOYÉE ===');
    console.log('URL:', config.url);
    console.log('Method:', config.method);
    console.log('Data:', config.data);
    console.log('='.repeat(30));
    return config;
  }
);

# 6. FRONTEND - Inspecter réponse reçue
────────────────────────────────────────
api.interceptors.response.use(
  (response) => {
    console.log('=== RÉPONSE REÇUE ===');
    console.log('Status:', response.status);
    console.log('Data:', response.data);
    console.log('='.repeat(30));
    return response;
  }
);

# 7. FRONTEND - Inspecter state React
──────────────────────────────────────
useEffect(() => {
    console.log('=== STATE USER ===');
    console.log('Type:', typeof user);
    console.log('Contenu:', user);
    console.log('Email:', user?.email);
    console.log('='.repeat(30));
}, [user]);


# === EXEMPLE COMPLET AVEC LOGS ===

# 1. FRONTEND - Soumission formulaire
──────────────────────────────────────
const handleSubmit = async (e) => {
    e.preventDefault();
    
    const formData = {
        username: username,  // "john"
        email: email,        // "user@example.com"
        password: password   // "pass123"
    };
    
    console.log('[SORTIE] Frontend envoie:', formData);
    
    try {
        const response = await authService.register(
            formData.username,
            formData.email,
            formData.password
        );
        
        console.log('[OK] Réponse reçue:', response);
    } catch (error) {
        console.error('[X] Erreur:', error.response?.data);
    }
};

# 2. BACKEND - Réception et traitement
───────────────────────────────────────
@auth_bp.route('/register', methods=['POST'])
def register():
    data = request.get_json()
    app.logger.info(f'[ENTREE] Backend reçoit: {data}')
    
    try:
        user = UserService.create_user(
            username=data['username'],
            email=data['email'],
            password=data['password']
        )
        app.logger.info(f'[OK] User créé: ID={user.id}, email={user.email}')
        
        response_data = {
            'message': 'Utilisateur créé avec succès',
            'user': user.to_dict()
        }
        app.logger.info(f'[SORTIE] Backend envoie: {response_data}')
        
        return jsonify(response_data), 201
    
    except ValueError as e:
        app.logger.error(f'[X] Erreur: {str(e)}')
        return jsonify({'error': str(e)}), 400

# 3. SERVICE - Création utilisateur
────────────────────────────────────
def create_user(username, email, password):
    app.logger.info(f'[OUTIL] Service.create_user appelé')
    app.logger.info(f'   username={username}, email={email}')
    
    # Vérifier si existe
    existing = User.query.filter_by(email=email).first()
    if existing:
        app.logger.warning(f'[ATTENTION]  Email {email} déjà utilisé')
        raise ValueError("Email déjà utilisé")
    
    # Créer
    user = User(username=username, email=email, ...)
    app.logger.info(f'[SAUVEGARDE] Sauvegarde en DB...')
    
    db.session.add(user)
    db.session.commit()
    
    app.logger.info(f'[OK] User sauvegardé: ID={user.id}')
    return user


# === RÉSUMÉ: CHECKLIST DE CORRESPONDANCE ===

Pour chaque donnée qui traverse les tiers, vérifiez:

[WHITE_SQUARE] DATABASE
  [WHITE_SQUARE] Nom de colonne défini (ex: email VARCHAR(100))
  [WHITE_SQUARE] Type SQL approprié
  [WHITE_SQUARE] Contraintes définies (UNIQUE, NOT NULL)
  
[WHITE_SQUARE] BACKEND - Modèle ORM
  [WHITE_SQUARE] Attribut Python = nom colonne SQL
  [WHITE_SQUARE] Type Python = type SQL (via db.Column)
  [WHITE_SQUARE] Relations définies si nécessaire
  
[WHITE_SQUARE] BACKEND - Sérialisation (to_dict)
  [WHITE_SQUARE] Conversion types spéciaux (Decimal -> float, datetime -> isoformat)
  [WHITE_SQUARE] Gestion des valeurs None
  [WHITE_SQUARE] Nommage cohérent (snake_case -> camelCase si besoin)
  
[WHITE_SQUARE] API JSON
  [WHITE_SQUARE] Noms de clés cohérents
  [WHITE_SQUARE] Types JSON valides (string, number, boolean, object, array, null)
  
[WHITE_SQUARE] FRONTEND - Désérialisation
  [WHITE_SQUARE] Vérification existence des clés
  [WHITE_SQUARE] Conversion types si nécessaire (ISO string -> Date)
  [WHITE_SQUARE] Gestion des valeurs null/undefined
  
[WHITE_SQUARE] FRONTEND - State
  [WHITE_SQUARE] Nommage cohérent avec API
  [WHITE_SQUARE] Types TypeScript/PropTypes si utilisés
  [WHITE_SQUARE] Valeurs par défaut pour éviter undefined


# === OUTILS POUR FACILITER LA CORRESPONDANCE ===

# 1. Marshmallow (Backend - Validation + Sérialisation)
────────────────────────────────────────────────────────
pip install marshmallow

from marshmallow import Schema, fields

class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    email = fields.Email(required=True)
    created_at = fields.DateTime(dump_only=True, data_key='createdAt')
    #                                            ^ snake_case -> camelCase

user_schema = UserSchema()

# Sérialisation automatique
user_dict = user_schema.dump(user)
# {'id': 1, 'username': 'john', 'email': '...', 'createdAt': '2024...'}

# Désérialisation + validation
try:
    user_data = user_schema.load(request.get_json())
except ValidationError as err:
    return jsonify(err.messages), 400


# 2. humps (Conversion snake_case <-> camelCase)
───────────────────────────────────────────────
pip install pyhumps

import humps

# camelCase -> snake_case
data = {'firstName': 'John', 'lastName': 'Doe'}
snake_data = humps.decamelize(data)
# {'first_name': 'John', 'last_name': 'Doe'}

# snake_case -> camelCase
data = {'first_name': 'John', 'last_name': 'Doe'}
camel_data = humps.camelize(data)
# {'firstName': 'John', 'lastName': 'Doe'}

# Utilisation dans Flask
@app.after_request
def convert_to_camel_case(response):
    if response.is_json:
        response.set_data(
            humps.camelize(response.get_json())
        )
    return response


# 3. TypeScript (Frontend - Typage fort)
────────────────────────────────────────
// Définir interfaces qui correspondent au backend
interface User {
    id: number;
    username: string;
    email: string;
    createdAt: string;  // ISO date string
}

interface Product {
    id: number;
    productName: string;
    unitPrice: number;
    stockQuantity: number;
}

// Utilisation avec typage
const getUser = async (id: number): Promise<User> => {
    const response = await api.get<User>(`/users/${id}`);
    return response.data;
};


# === FLUX DE DONNÉES ===

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  1. Utilisateur clique sur "Se connecter"                       │
│     Frontend (React)                                            │
│                                                                 │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             v HTTP POST /api/auth/login
                             │ Body: { email, password }
                             │
┌────────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                                                                 │
│  2. Flask reçoit la requête                                     │
│     Backend (Flask)                                             │
│     - Route auth.py: login()                                    │
│     - Service auth_service.py: authenticate()                   │
│                                                                 │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             v SQL SELECT
                             │ SELECT * FROM users WHERE email = ?
                             │
┌────────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                                                                 │
│  3. PostgreSQL recherche l'utilisateur                          │
│     Database (PostgreSQL)                                       │
│     - Vérifie email                                             │
│     - Retourne données utilisateur                              │
│                                                                 │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             v Données utilisateur
                             │
┌────────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                                                                 │
│  4. Flask traite les données                                    │
│     Backend (Flask)                                             │
│     - Vérifie mot de passe (bcrypt)                             │
│     - Génère JWT token                                          │
│     - Prépare réponse JSON                                      │
│                                                                 │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             v HTTP 200 OK
                             │ Body: { access_token, user }
                             │
┌────────────────────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────────────────────────────┐
│                                                                 │
│  5. React reçoit la réponse                                     │
│     Frontend (React)                                            │
│     - Stocke token dans localStorage                            │
│     - Met à jour state (Context API)                            │
│     - Redirige vers /products                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘


# === FORMATS D'ÉCHANGE (JSON) ===

# Requête login
POST /api/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "secretpassword"
}

# Réponse login
HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "Connexion réussie",
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 1,
    "username": "john_doe",
    "email": "user@example.com",
    "created_at": "2024-01-15T10:30:00"
  }
}

# Requête avec authentification
GET /api/products
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# Réponse liste produits
HTTP/1.1 200 OK
Content-Type: application/json

{
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "description": "High-performance laptop",
      "price": 1299.99,
      "stock": 15,
      "created_at": "2024-01-10T14:20:00"
    }
  ],
  "total": 50,
  "pages": 5,
  "current_page": 1
}


[OK] DÉPLOIEMENT AVEC DOCKER


# === DOCKERFILE BACKEND ===

# backend/Dockerfile

FROM python:3.11-slim

WORKDIR /app

# Dépendances système
RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Dépendances Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Code application
COPY . .

EXPOSE 5000

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "run:app"]


# === DOCKERFILE FRONTEND ===

# frontend/Dockerfile

FROM node:18-alpine AS build

WORKDIR /app

# Dépendances
COPY package*.json ./
RUN npm ci

# Build
COPY . .
RUN npm run build

# Production
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]


# frontend/nginx.conf

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_pass http://backend:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}


# === DOCKER COMPOSE ===

# docker-compose.yml

version: '3.8'

services:
  # Database
  postgres:
    image: postgres:15
    container_name: myapp-postgres
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: myapp_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    networks:
      - myapp-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Backend
  backend:
    build: ./backend
    container_name: myapp-backend
    environment:
      DATABASE_URL: postgresql://myuser:mypassword@postgres:5432/myapp_db
      FLASK_ENV: production
      SECRET_KEY: ${SECRET_KEY}
      JWT_SECRET_KEY: ${JWT_SECRET_KEY}
      CORS_ORIGINS: http://localhost:3000
    ports:
      - "5000:5000"
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - myapp-network
    volumes:
      - ./backend:/app
    restart: unless-stopped

  # Frontend
  frontend:
    build: ./frontend
    container_name: myapp-frontend
    ports:
      - "3000:80"
    depends_on:
      - backend
    networks:
      - myapp-network
    restart: unless-stopped

volumes:
  postgres_data:

networks:
  myapp-network:
    driver: bridge


# === COMMANDES DOCKER ===

# Build et démarrer tous les services
docker-compose up --build

# Démarrer en arrière-plan
docker-compose up -d

# Voir les logs
docker-compose logs -f

# Arrêter les services
docker-compose down

# Arrêter et supprimer volumes
docker-compose down -v

# Reconstruire un service spécifique
docker-compose up --build backend

# Exécuter commande dans un container
docker-compose exec backend flask shell
docker-compose exec postgres psql -U myuser -d myapp_db


[OK] TESTS


# === TESTS BACKEND ===

# backend/tests/test_auth.py

import pytest
from app import create_app
from app.database import db
from app.models import User

@pytest.fixture
def app():
    app = create_app('testing')
    with app.app_context():
        db.create_all()
        yield app
        db.session.remove()
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

def test_register(client):
    """Test inscription utilisateur"""
    response = client.post('/api/auth/register', json={
        'username': 'testuser',
        'email': 'test@example.com',
        'password': 'testpass123'
    })
    
    assert response.status_code == 201
    data = response.get_json()
    assert data['message'] == 'Utilisateur créé avec succès'
    assert 'user' in data

def test_login(client):
    """Test connexion utilisateur"""
    # Créer utilisateur
    client.post('/api/auth/register', json={
        'username': 'testuser',
        'email': 'test@example.com',
        'password': 'testpass123'
    })
    
    # Login
    response = client.post('/api/auth/login', json={
        'email': 'test@example.com',
        'password': 'testpass123'
    })
    
    assert response.status_code == 200
    data = response.get_json()
    assert 'access_token' in data

def test_login_invalid(client):
    """Test login avec mauvais identifiants"""
    response = client.post('/api/auth/login', json={
        'email': 'wrong@example.com',
        'password': 'wrongpass'
    })
    
    assert response.status_code == 401


# Lancer les tests
pytest
pytest -v
pytest --cov=app


# === TESTS FRONTEND ===

# frontend/src/services/__tests__/authService.test.js

import { describe, it, expect, beforeEach, vi } from 'vitest';
import authService from '../authService';
import api from '../api';

vi.mock('../api');

describe('authService', () => {
  beforeEach(() => {
    localStorage.clear();
    vi.clearAllMocks();
  });

  it('should register user', async () => {
    const mockResponse = {
      data: {
        message: 'User created',
        user: { id: 1, username: 'test', email: 'test@example.com' }
      }
    };
    
    api.post.mockResolvedValue(mockResponse);
    
    const result = await authService.register('test', 'test@example.com', 'pass');
    
    expect(api.post).toHaveBeenCalledWith('/auth/register', {
      username: 'test',
      email: 'test@example.com',
      password: 'pass'
    });
    expect(result).toEqual(mockResponse.data);
  });

  it('should login user and store token', async () => {
    const mockResponse = {
      data: {
        access_token: 'fake-token',
        user: { id: 1, username: 'test' }
      }
    };
    
    api.post.mockResolvedValue(mockResponse);
    
    await authService.login('test@example.com', 'pass');
    
    expect(localStorage.getItem('access_token')).toBe('fake-token');
    expect(localStorage.getItem('user')).toBe(JSON.stringify({ id: 1, username: 'test' }));
  });
});


# Lancer les tests frontend
npm test
npm run test:coverage


[OK] BONNES PRATIQUES


# 1. SÉPARATION DES RESPONSABILITÉS
[OK] Frontend ne contient AUCUNE logique métier
[OK] Backend ne génère AUCUN HTML
[OK] Database accessible UNIQUEMENT par le backend

# 2. SÉCURITÉ
[OK] CORS configuré correctement
[OK] Authentification JWT
[OK] Validation des données côté backend
[OK] Mots de passe hashés (bcrypt)
[OK] Variables d'environnement pour secrets
[OK] HTTPS en production
[OK] Rate limiting sur API

# 3. GESTION DES ERREURS
[OK] Try-catch partout
[OK] Messages d'erreur clairs
[OK] Codes HTTP appropriés
[OK] Logging des erreurs

# 4. PERFORMANCE
[OK] Pagination des listes
[OK] Index sur colonnes recherchées
[OK] Requêtes SQL optimisées
[OK] Caching si nécessaire
[OK] Compression des réponses

# 5. MAINTENABILITÉ
[OK] Code modulaire
[OK] Noms explicites
[OK] Documentation
[OK] Tests unitaires
[OK] Tests d'intégration

# 6. SCALABILITÉ
[OK] Stateless backend (JWT)
[OK] Database sur serveur dédié
[OK] Load balancing possible
[OK] CDN pour frontend statique


[OK] DÉPANNAGE


# === PROBLÈME: CORS Error ===

# Erreur
Access to XMLHttpRequest at 'http://localhost:5000/api/products' 
from origin 'http://localhost:3000' has been blocked by CORS policy

# Solution
# backend/app/__init__.py
from flask_cors import CORS
CORS(app, origins=['http://localhost:3000'])

# Ou en production
CORS(app, origins=['https://monapp.com'])


# === PROBLÈME: JWT Token expiré ===

# Erreur
401 Unauthorized

# Solution
# Augmenter durée du token
# backend/app/config.py
JWT_ACCESS_TOKEN_EXPIRES = 86400  # 24 heures

# Ou implémenter refresh token


# === PROBLÈME: Connection refused Database ===

# Erreur
sqlalchemy.exc.OperationalError: could not connect to server

# Solution
# Vérifier que PostgreSQL est démarré
docker ps
docker-compose up postgres

# Vérifier les credentials
# backend/.env
DATABASE_URL=postgresql://user:pass@host:port/dbname


# === PROBLÈME: Port déjà utilisé ===

# Erreur
Address already in use

# Solution Linux/Mac
lsof -ti:5000 | xargs kill -9  # Backend
lsof -ti:3000 | xargs kill -9  # Frontend

# Solution Windows
netstat -ano | findstr :5000
taskkill /PID <PID> /F


# === PROBLÈME: Module non trouvé ===

# Erreur Python
ModuleNotFoundError: No module named 'flask'

# Solution
# Vérifier environnement virtuel activé
which python
source .venv/bin/activate
pip install -r requirements.txt

# Erreur Node
Cannot find module 'axios'

# Solution
cd frontend
npm install


# === PROBLÈME: Migrations DB ===

# Erreur
Table doesn't exist

# Solution avec Flask-Migrate
flask db init
flask db migrate -m "Initial migration"
flask db upgrade

# Ou directement
python
>>> from app import create_app
>>> from app.database import db
>>> app = create_app()
>>> with app.app_context():
...     db.create_all()


[OK] ARCHITECTURE AVANCÉE


# === AJOUT D'UN CACHE (REDIS) ===

# Installer Redis
pip install redis flask-caching

# backend/app/config.py
CACHE_TYPE = 'redis'
CACHE_REDIS_URL = 'redis://localhost:6379/0'

# backend/app/__init__.py
from flask_caching import Cache

cache = Cache()

def create_app(config_name='default'):
    app = Flask(__name__)
    cache.init_app(app)
    return app

# Utilisation dans routes
from app import cache

@products_bp.route('', methods=['GET'])
@cache.cached(timeout=300, query_string=True)  # 5 minutes
def get_products():
    # ...
    return jsonify(products)

# Invalider cache après création/modification
@products_bp.route('', methods=['POST'])
@jwt_required()
def create_product():
    # Créer produit
    cache.delete('view//api/products')
    return jsonify(product)


# === AJOUT FILE D'ATTENTE (CELERY) ===

# Installer Celery
pip install celery redis

# backend/celery_app.py
from celery import Celery

celery = Celery(
    'myapp',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/0'
)

# Tâches asynchrones
@celery.task
def send_email(user_email, subject, body):
    """Envoyer email de façon asynchrone"""
    # Code d'envoi email
    pass

@celery.task
def process_order(order_id):
    """Traiter commande de façon asynchrone"""
    # Logique de traitement
    pass

# Utilisation
from celery_app import send_email

@auth_bp.route('/register', methods=['POST'])
def register():
    user = UserService.create_user(...)
    # Envoyer email de bienvenue en arrière-plan
    send_email.delay(user.email, 'Bienvenue', 'Merci de vous être inscrit')
    return jsonify(user.to_dict())

# Lancer worker Celery
celery -A celery_app worker --loglevel=info


# === LOGGING STRUCTURÉ ===

# backend/app/logging_config.py

import logging
import sys
from logging.handlers import RotatingFileHandler

def setup_logging(app):
    """Configure le logging de l'application"""
    
    # Format
    formatter = logging.Formatter(
        '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
    )
    
    # Handler fichier
    file_handler = RotatingFileHandler(
        'logs/app.log',
        maxBytes=10240000,  # 10MB
        backupCount=10
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)
    
    # Handler console
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setFormatter(formatter)
    console_handler.setLevel(logging.DEBUG)
    
    # Configuration app
    app.logger.addHandler(file_handler)
    app.logger.addHandler(console_handler)
    app.logger.setLevel(logging.INFO)
    
    app.logger.info('Application démarrée')

# Utilisation dans routes
@products_bp.route('', methods=['POST'])
def create_product():
    app.logger.info(f'Création produit par utilisateur {user_id}')
    # ...


# === MONITORING & HEALTHCHECKS ===

# backend/app/routes/health.py

from flask import Blueprint, jsonify
from app.database import db
from sqlalchemy import text

health_bp = Blueprint('health', __name__)

@health_bp.route('/health', methods=['GET'])
def health_check():
    """Vérification santé de l'application"""
    return jsonify({
        'status': 'ok',
        'service': 'backend'
    }), 200

@health_bp.route('/health/ready', methods=['GET'])
def readiness_check():
    """Vérification que l'app est prête à recevoir du trafic"""
    checks = {
        'database': False,
        'cache': False
    }
    
    # Vérifier DB
    try:
        db.session.execute(text('SELECT 1'))
        checks['database'] = True
    except Exception as e:
        pass
    
    # Vérifier cache
    try:
        from app import cache
        cache.get('health_check')
        checks['cache'] = True
    except Exception as e:
        pass
    
    all_ok = all(checks.values())
    status_code = 200 if all_ok else 503
    
    return jsonify({
        'ready': all_ok,
        'checks': checks
    }), status_code

@health_bp.route('/health/live', methods=['GET'])
def liveness_check():
    """Vérification que l'app est vivante"""
    return jsonify({'alive': True}), 200


# === RATE LIMITING ===

# Installer Flask-Limiter
pip install Flask-Limiter

# backend/app/__init__.py
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

# Utilisation sur routes
@auth_bp.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
    # ...

@products_bp.route('', methods=['GET'])
@limiter.limit("100 per minute")
def get_products():
    # ...


# === VALIDATION AVANCÉE ===

# Installer Marshmallow
pip install marshmallow

# backend/app/schemas.py

from marshmallow import Schema, fields, validate, validates, ValidationError

class UserSchema(Schema):
    id = fields.Int(dump_only=True)
    username = fields.Str(
        required=True,
        validate=validate.Length(min=3, max=50)
    )
    email = fields.Email(required=True)
    password = fields.Str(
        required=True,
        load_only=True,
        validate=validate.Length(min=8)
    )
    created_at = fields.DateTime(dump_only=True)
    
    @validates('username')
    def validate_username(self, value):
        if not value.isalnum():
            raise ValidationError('Username doit être alphanumérique')

class ProductSchema(Schema):
    id = fields.Int(dump_only=True)
    name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
    description = fields.Str()
    price = fields.Decimal(
        required=True,
        validate=validate.Range(min=0)
    )
    stock = fields.Int(validate=validate.Range(min=0))
    created_by = fields.Int(dump_only=True)
    created_at = fields.DateTime(dump_only=True)

# Utilisation
from app.schemas import ProductSchema

product_schema = ProductSchema()
products_schema = ProductSchema(many=True)

@products_bp.route('', methods=['POST'])
@jwt_required()
def create_product():
    try:
        # Valider et désérialiser
        data = product_schema.load(request.get_json())
        
        # Créer produit
        product = ProductService.create_product(**data)
        
        # Sérialiser réponse
        return product_schema.dump(product), 201
    
    except ValidationError as err:
        return jsonify({'errors': err.messages}), 400


# === WEBSOCKETS (TEMPS RÉEL) ===

# Installer Flask-SocketIO
pip install flask-socketio python-socketio

# backend/app/__init__.py
from flask_socketio import SocketIO

socketio = SocketIO(cors_allowed_origins="*")

def create_app(config_name='default'):
    app = Flask(__name__)
    socketio.init_app(app)
    return app

# backend/app/events.py
from flask_socketio import emit, join_room, leave_room
from flask_jwt_extended import decode_token
from app import socketio

@socketio.on('connect')
def handle_connect(auth):
    """Connexion WebSocket"""
    try:
        token = auth.get('token')
        user_id = decode_token(token)['sub']
        join_room(f'user_{user_id}')
        emit('connected', {'message': 'Connecté'})
    except:
        return False

@socketio.on('disconnect')
def handle_disconnect():
    """Déconnexion WebSocket"""
    pass

@socketio.on('join_order_updates')
def handle_join_orders(data):
    """S'abonner aux mises à jour commandes"""
    user_id = data['user_id']
    join_room(f'orders_{user_id}')

# Émettre événement depuis service
from app import socketio

def update_order_status(order_id, status):
    order = OrderService.update_order_status(order_id, status)
    
    # Notifier utilisateur en temps réel
    socketio.emit(
        'order_updated',
        {'order': order.to_dict()},
        room=f'orders_{order.user_id}'
    )
    
    return order

# Frontend - Installation
npm install socket.io-client

// frontend/src/services/socketService.js
import { io } from 'socket.io-client';
import authService from './authService';

class SocketService {
  constructor() {
    this.socket = null;
  }

  connect() {
    const token = authService.getToken();
    
    this.socket = io('http://localhost:5000', {
      auth: { token }
    });

    this.socket.on('connect', () => {
      console.log('WebSocket connecté');
    });

    this.socket.on('order_updated', (data) => {
      console.log('Commande mise à jour:', data);
      // Mettre à jour UI
    });
  }

  disconnect() {
    if (this.socket) {
      this.socket.disconnect();
    }
  }

  joinOrderUpdates(userId) {
    this.socket.emit('join_order_updates', { user_id: userId });
  }
}

export default new SocketService();


[OK] PATTERNS AVANCÉS


# === REPOSITORY PATTERN ===

# backend/app/repositories/user_repository.py

class UserRepository:
    """Abstraction de l'accès aux données utilisateurs"""
    
    @staticmethod
    def find_by_id(user_id):
        return User.query.get(user_id)
    
    @staticmethod
    def find_by_email(email):
        return User.query.filter_by(email=email).first()
    
    @staticmethod
    def find_all(page=1, per_page=10):
        return User.query.paginate(page=page, per_page=per_page)
    
    @staticmethod
    def save(user):
        db.session.add(user)
        db.session.commit()
        return user
    
    @staticmethod
    def delete(user):
        db.session.delete(user)
        db.session.commit()

# Service utilise repository
class UserService:
    
    @staticmethod
    def create_user(username, email, password):
        # Vérifier existence
        if UserRepository.find_by_email(email):
            raise ValueError("Email déjà utilisé")
        
        # Créer et sauvegarder
        user = User(username=username, email=email, ...)
        return UserRepository.save(user)


# === FACTORY PATTERN ===

# backend/app/factories/order_factory.py

class OrderFactory:
    """Factory pour créer différents types de commandes"""
    
    @staticmethod
    def create_standard_order(user_id, items):
        """Commande standard"""
        return Order(
            user_id=user_id,
            items=items,
            shipping_type='standard',
            processing_time=3
        )
    
    @staticmethod
    def create_express_order(user_id, items):
        """Commande express"""
        return Order(
            user_id=user_id,
            items=items,
            shipping_type='express',
            processing_time=1,
            extra_fee=10.00
        )
    
    @staticmethod
    def create_order(user_id, items, order_type='standard'):
        """Factory method principal"""
        if order_type == 'express':
            return OrderFactory.create_express_order(user_id, items)
        return OrderFactory.create_standard_order(user_id, items)


# === DECORATOR PATTERN ===

# backend/app/decorators.py

from functools import wraps
from flask import jsonify
from flask_jwt_extended import get_jwt_identity

def admin_required(f):
    """Décorateur pour routes admin uniquement"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        user_id = get_jwt_identity()
        user = User.query.get(user_id)
        
        if not user or not user.is_admin:
            return jsonify({'error': 'Admin requis'}), 403
        
        return f(*args, **kwargs)
    
    return decorated_function

def validate_json(*expected_args):
    """Décorateur pour valider présence de champs JSON"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            json_data = request.get_json()
            
            if not json_data:
                return jsonify({'error': 'JSON requis'}), 400
            
            for arg in expected_args:
                if arg not in json_data:
                    return jsonify({'error': f'{arg} manquant'}), 400
            
            return f(*args, **kwargs)
        
        return decorated_function
    return decorator

# Utilisation
@products_bp.route('', methods=['POST'])
@jwt_required()
@admin_required
@validate_json('name', 'price')
def create_product():
    # ...


# === STRATEGY PATTERN ===

# backend/app/strategies/payment_strategy.py

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    """Interface pour stratégies de paiement"""
    
    @abstractmethod
    def process_payment(self, amount, order_id):
        pass

class StripePayment(PaymentStrategy):
    def process_payment(self, amount, order_id):
        # Logique Stripe
        return {'status': 'success', 'provider': 'stripe'}

class PayPalPayment(PaymentStrategy):
    def process_payment(self, amount, order_id):
        # Logique PayPal
        return {'status': 'success', 'provider': 'paypal'}

class CryptoPayment(PaymentStrategy):
    def process_payment(self, amount, order_id):
        # Logique crypto
        return {'status': 'success', 'provider': 'crypto'}

# Service de paiement
class PaymentService:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy
    
    def pay(self, amount, order_id):
        return self.strategy.process_payment(amount, order_id)

# Utilisation
@orders_bp.route('/<int:order_id>/pay', methods=['POST'])
@jwt_required()
def pay_order(order_id):
    data = request.get_json()
    payment_method = data.get('payment_method', 'stripe')
    
    # Choisir stratégie
    if payment_method == 'stripe':
        strategy = StripePayment()
    elif payment_method == 'paypal':
        strategy = PayPalPayment()
    elif payment_method == 'crypto':
        strategy = CryptoPayment()
    else:
        return jsonify({'error': 'Méthode invalide'}), 400
    
    # Exécuter paiement
    payment_service = PaymentService(strategy)
    result = payment_service.pay(order.total_amount, order_id)
    
    return jsonify(result), 200


[OK] PERFORMANCES & OPTIMISATION


# === REQUÊTES N+1 ===

# [X] MAUVAIS - Requête N+1
@orders_bp.route('', methods=['GET'])
def get_orders():
    orders = Order.query.all()
    
    # Pour chaque commande, une requête supplémentaire pour items
    return jsonify([order.to_dict() for order in orders])

# [OK] BON - Eager loading
@orders_bp.route('', methods=['GET'])
def get_orders():
    orders = Order.query.options(
        db.joinedload(Order.items).joinedload(OrderItem.product)
    ).all()
    
    return jsonify([order.to_dict() for order in orders])


# === INDEX DATABASE ===

# backend/app/models.py

class Product(db.Model):
    # ...
    
    __table_args__ = (
        db.Index('idx_product_name_price', 'name', 'price'),
        db.Index('idx_product_created_at', 'created_at'),
    )

# Créer index manuellement
CREATE INDEX idx_products_name ON products(name);
CREATE INDEX idx_products_price ON products(price);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);


# === COMPRESSION RESPONSES ===

# Installer Flask-Compress
pip install Flask-Compress

# backend/app/__init__.py
from flask_compress import Compress

def create_app(config_name='default'):
    app = Flask(__name__)
    Compress(app)
    return app


# === PAGINATION PERFORMANTE ===

# [X] MAUVAIS - OFFSET/LIMIT lent sur grandes tables
products = Product.query.offset(page * per_page).limit(per_page).all()

# [OK] BON - Cursor-based pagination
@products_bp.route('', methods=['GET'])
def get_products():
    cursor = request.args.get('cursor')  # Dernier ID vu
    per_page = request.args.get('per_page', 10, type=int)
    
    query = Product.query
    
    if cursor:
        query = query.filter(Product.id > cursor)
    
    products = query.order_by(Product.id).limit(per_page + 1).all()
    
    has_more = len(products) > per_page
    products = products[:per_page]
    
    next_cursor = products[-1].id if products and has_more else None
    
    return jsonify({
        'products': [p.to_dict() for p in products],
        'next_cursor': next_cursor,
        'has_more': has_more
    })


# === CACHING INTELLIGENT ===

# Cache résultats coûteux
@cache.memoize(timeout=3600)
def get_product_stats(product_id):
    """Statistiques produit (calcul lourd)"""
    # Calculs complexes
    return stats

# Invalider cache sélectivement
@products_bp.route('/<int:product_id>', methods=['PUT'])
def update_product(product_id):
    product = ProductService.update_product(product_id, **data)
    
    # Invalider seulement le cache de ce produit
    cache.delete_memoized(get_product_stats, product_id)
    
    return jsonify(product.to_dict())


[OK] SÉCURITÉ AVANCÉE


# === PROTECTION CSRF ===

# Installer Flask-WTF
pip install Flask-WTF

# backend/app/__init__.py
from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect()

def create_app(config_name='default'):
    app = Flask(__name__)
    csrf.init_app(app)
    
    # Exempter API routes (on utilise JWT)
    csrf.exempt(auth_bp)
    csrf.exempt(products_bp)
    csrf.exempt(orders_bp)
    
    return app


# === VALIDATION INPUT ===

# Sanitization
from bleach import clean

@products_bp.route('', methods=['POST'])
def create_product():
    data = request.get_json()
    
    # Nettoyer input HTML
    data['name'] = clean(data['name'], strip=True)
    data['description'] = clean(data['description'], strip=True)
    
    # ...


# === RATE LIMITING PAR UTILISATEUR ===

from flask_limiter import Limiter

limiter = Limiter(
    key_func=lambda: get_jwt_identity() if get_jwt_identity() else get_remote_address()
)

@orders_bp.route('', methods=['POST'])
@jwt_required()
@limiter.limit("10 per hour", key_func=lambda: get_jwt_identity())
def create_order():
    # Limite de 10 commandes/heure par utilisateur
    pass


# === SECRETS MANAGEMENT ===

# Ne JAMAIS commit .env
# Utiliser des secrets managers en production

# AWS Secrets Manager
import boto3

def get_secret(secret_name):
    client = boto3.client('secretsmanager')
    response = client.get_secret_value(SecretId=secret_name)
    return response['SecretString']

# Ou variables d'environnement système
DATABASE_URL = os.environ.get('DATABASE_URL')


[OK] RESSOURCES & DOCUMENTATION


# === Documentation API (Swagger) ===

# Installer Flasgger
pip install flasgger

# backend/app/__init__.py
from flasgger import Swagger

def create_app(config_name='default'):
    app = Flask(__name__)
    
    swagger_config = {
        "headers": [],
        "specs": [
            {
                "endpoint": 'apispec',
                "route": '/apispec.json',
                "rule_filter": lambda rule: True,
                "model_filter": lambda tag: True,
            }
        ],
        "static_url_path": "/flasgger_static",
        "swagger_ui": True,
        "specs_route": "/docs"
    }
    
    Swagger(app, config=swagger_config)
    
    return app

# Documentation d'un endpoint
@products_bp.route('/<int:product_id>', methods=['GET'])
def get_product(product_id):
    """
    Récupère un produit par ID
    ---
    parameters:
      - name: product_id
        in: path
        type: integer
        required: true
        description: ID du produit
    responses:
      200:
        description: Produit trouvé
        schema:
          properties:
            id:
              type: integer
            name:
              type: string
            price:
              type: number
      404:
        description: Produit non trouvé
    """
    # ...

# Accéder à la doc: http://localhost:5000/docs


# === README.md Complet ===

# MyApp - Application Trois Tiers

## Architecture

```
Frontend (React) -> Backend (Flask) -> Database (PostgreSQL)
```

## Installation

### Prérequis
- Python 3.11+
- Node.js 18+
- PostgreSQL 15+

### Backend
```bash
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Éditer .env avec vos configurations
flask db upgrade
python run.py
```

### Frontend
```bash
cd frontend
npm install
cp .env.example .env
# Éditer .env
npm run dev
```

### Docker
```bash
docker-compose up --build
```

## Tests
```bash
# Backend
cd backend
pytest

# Frontend
cd frontend
npm test
```

## Documentation
- API: http://localhost:5000/docs
- Frontend: http://localhost:3000

## Structure
Voir ARCHITECTURE.md pour détails complets

## Licence
MIT


[OK] CHECKLIST DÉPLOIEMENT PRODUCTION


[WHITE_SQUARE] BACKEND
  [WHITE_SQUARE] Variables d'environnement configurées
  [WHITE_SQUARE] SECRET_KEY et JWT_SECRET_KEY forts
  [WHITE_SQUARE] DEBUG = False
  [WHITE_SQUARE] CORS configuré avec origines spécifiques
  [WHITE_SQUARE] HTTPS activé
  [WHITE_SQUARE] Rate limiting activé
  [WHITE_SQUARE] Logging configuré
  [WHITE_SQUARE] Migrations DB exécutées
  [WHITE_SQUARE] Gunicorn ou uWSGI configuré
  [WHITE_SQUARE] Reverse proxy (Nginx) configuré
  [WHITE_SQUARE] Certificat SSL installé
  [WHITE_SQUARE] Monitoring (Sentry, New Relic)
  [WHITE_SQUARE] Backups DB automatisés

[WHITE_SQUARE] FRONTEND
  [WHITE_SQUARE] Build production créé (npm run build)
  [WHITE_SQUARE] Variables d'environnement production
  [WHITE_SQUARE] HTTPS activé
  [WHITE_SQUARE] CDN configuré (CloudFlare, AWS CloudFront)
  [WHITE_SQUARE] Compression activée (Gzip, Brotli)
  [WHITE_SQUARE] Cache headers configurés
  [WHITE_SQUARE] Error tracking (Sentry)
  [WHITE_SQUARE] Analytics (Google Analytics, Plausible)

[WHITE_SQUARE] DATABASE
  [WHITE_SQUARE] Serveur dédié ou managé (AWS RDS, DigitalOcean)
  [WHITE_SQUARE] Backups automatiques
  [WHITE_SQUARE] Réplication configurée
  [WHITE_SQUARE] Index optimisés
  [WHITE_SQUARE] Connection pooling configuré
  [WHITE_SQUARE] Firewall: accès backend uniquement

[WHITE_SQUARE] SÉCURITÉ
  [WHITE_SQUARE] Scan vulnérabilités (Snyk, OWASP ZAP)
  [WHITE_SQUARE] Secrets management (AWS Secrets Manager)
  [WHITE_SQUARE] WAF configuré
  [WHITE_SQUARE] DDoS protection
  [WHITE_SQUARE] Audit logs activés

[WHITE_SQUARE] MONITORING
  [WHITE_SQUARE] Healthchecks configurés
  [WHITE_SQUARE] Alertes configurées (PagerDuty, Opsgenie)
  [WHITE_SQUARE] Métriques collectées (Prometheus, Datadog)
  [WHITE_SQUARE] Logs centralisés (ELK, CloudWatch)


[OK] FIN DU GUIDE

Ce guide couvre tous les aspects de l'architecture trois tiers avec Flask et React:
- Compréhension théorique
- Implémentation complète backend et frontend  
- Communication entre les tiers
- Sécurité et authentification
- Tests et déploiement
- Patterns avancés
- Optimisations
- Production ready

Pour aller plus loin:
- Microservices architecture
- GraphQL au lieu de REST
- Server-Side Rendering (Next.js)
- Progressive Web Apps (PWA)
- Kubernetes pour orchestration
