# ARCHITECTURE.md — ICAVI GESTION
*Rédigé le 2026-10-08 — basé sur l'audit AUDIT.md*

---

## 1. ARCHITECTURE CIBLE

### Vue d'ensemble

```
┌─────────────────────────────────────────────────────────┐
│                    UTILISATEUR (navigateur)              │
└───────────────────────┬─────────────────────────────────┘
                        │ HTTPS
         ┌──────────────▼──────────────┐
         │   React SPA (fichiers       │
         │   statiques servis par OVH) │
         │   gst.icavi-secure.com      │
         └──────────────┬──────────────┘
                        │ HTTPS / JSON (API REST)
         ┌──────────────▼──────────────┐
         │   Laravel 12 API            │
         │   api.icavi-secure.com      │
         │   (ou gst.icavi-secure.com  │
         │    /api/v1/*)               │
         └──────────────┬──────────────┘
                        │
         ┌──────────────▼──────────────┐
         │   MySQL (icaviss3)          │
         │   OVH base de données       │
         └─────────────────────────────┘
```

### Décision sur les domaines

**Option A (recommandée) — même domaine, chemins séparés :**
```
gst.icavi-secure.com/          → fichiers React statiques
gst.icavi-secure.com/api/v1/   → Laravel API
```
Avantages : pas de CORS, cookies Sanctum SPA fonctionnent sans configuration spéciale, un seul certificat SSL, déploiement simplifié sur OVH.

**Option B — sous-domaines distincts :**
```
gst.icavi-secure.com      → React
api.gst.icavi-secure.com  → Laravel
```
Nécessite CORS + configuration `SESSION_DOMAIN` + `SANCTUM_STATEFUL_DOMAINS` sur OVH mutualisé.

→ **Retenir Option A** pour la compatibilité OVH mutualisé.

---

## 2. STRUCTURE DES DOSSIERS DU NOUVEAU PROJET

```
icavi-gestion/
│
├── backend/                    ← Laravel 12
│   ├── app/
│   │   ├── Http/
│   │   │   ├── Controllers/
│   │   │   │   └── Api/
│   │   │   │       └── V1/
│   │   │   │           ├── Auth/
│   │   │   │           ├── Dashboard/
│   │   │   │           ├── Clients/
│   │   │   │           ├── Products/
│   │   │   │           ├── Stock/
│   │   │   │           ├── Sales/
│   │   │   │           ├── Documents/
│   │   │   │           ├── CashVouchers/
│   │   │   │           ├── Permissions/
│   │   │   │           ├── Ged/
│   │   │   │           └── Users/
│   │   │   ├── Middleware/
│   │   │   └── Requests/          ← Form Requests par module
│   │   ├── Models/
│   │   ├── Policies/              ← Laravel Policies
│   │   ├── Resources/             ← API Resources (sérialisation JSON)
│   │   │   └── Api/V1/
│   │   ├── Services/              ← Logique métier (StockService, DocumentService...)
│   │   ├── Observers/
│   │   ├── Notifications/
│   │   ├── Helpers/
│   │   └── Providers/
│   ├── config/
│   ├── database/
│   │   ├── migrations/
│   │   └── seeders/
│   ├── routes/
│   │   ├── api.php                ← Toutes les routes API v1
│   │   └── web.php                ← Minimal : route catch-all pour SPA + print views
│   ├── resources/
│   │   └── views/
│   │       └── print/             ← Vues Blade uniquement pour PDF/impression
│   ├── storage/
│   ├── public/                    ← Document root OVH
│   │   ├── index.php
│   │   ├── dist/                  ← Build React (copié ici au déploiement)
│   │   └── storage → ../storage/app/public
│   └── .htaccess                  ← Réécriture URL pour SPA + API
│
├── frontend/                   ← React 18 + Vite + TypeScript
│   ├── src/
│   │   ├── api/                   ← Clients API (axios instances, endpoints)
│   │   ├── components/
│   │   │   ├── ui/                ← Composants atomiques (Button, Badge, Input...)
│   │   │   ├── layout/            ← Sidebar, Topbar, PageHeader...
│   │   │   └── shared/            ← DataTable, Modal, Form, Pagination...
│   │   ├── features/              ← Modules métier
│   │   │   ├── auth/
│   │   │   ├── dashboard/
│   │   │   ├── clients/
│   │   │   ├── products/
│   │   │   ├── stock/
│   │   │   ├── sales/
│   │   │   ├── documents/
│   │   │   ├── cash-vouchers/
│   │   │   ├── permissions/
│   │   │   ├── ged/
│   │   │   └── users/
│   │   ├── hooks/                 ← Hooks réutilisables
│   │   ├── lib/                   ← axios, queryClient, helpers
│   │   ├── stores/                ← Zustand (auth store uniquement)
│   │   ├── types/                 ← Types TypeScript partagés
│   │   └── main.tsx
│   ├── public/
│   ├── index.html
│   ├── vite.config.ts
│   ├── tailwind.config.ts
│   └── tsconfig.json
│
├── docs/
│   ├── AUDIT.md
│   ├── ARCHITECTURE.md
│   ├── DESIGN-SYSTEM.md
│   └── MIGRATION-PLAN.md
│
└── README.md
```

---

## 3. FRONTEND — REACT SPA

### Stack technique

| Outil | Version | Rôle | Justification |
|---|---|---|---|
| React | 18.3 | UI framework | Standard, grande communauté |
| Vite | 5.x | Build tool | Rapide, produit des fichiers statiques |
| TypeScript | 5.x | Typage | Détection d'erreurs, refactoring sûr |
| React Router | 6.x | Navigation SPA | Routing côté client |
| TanStack Query | 5.x | Cache API | Gestion état serveur, cache, revalidation |
| Tailwind CSS | 3.x | Styles | Utilitaires, dark mode, composants rapides |
| shadcn/ui | latest | Composants | Headless, accessible, pas de vendor lock-in |
| Zustand | 4.x | État global | Léger, pour auth + préférences uniquement |
| React Hook Form | 7.x | Formulaires | Performant, non-contrôlé |
| Zod | 3.x | Validation | Typage + validation runtime cohérents |
| Axios | 1.x | HTTP client | Interceptors, gestion tokens |
| Recharts | 2.x | Graphiques | React-natif, léger, dashboard |
| date-fns | 3.x | Dates | Léger, tree-shakeable |
| lucide-react | latest | Icônes | Déjà utilisé dans le projet actuel |

**Pas de :** Redux (Zustand suffit), MUI (trop lourd), Next.js (pas de SSR nécessaire, fichiers statiques voulus).

### Organisation d'une feature

```
features/documents/
├── api/
│   ├── documentApi.ts       ← Fonctions fetch (axios)
│   └── documentTypes.ts     ← Types TypeScript des réponses API
├── components/
│   ├── DocumentTable.tsx
│   ├── DocumentForm.tsx
│   ├── DocumentStatusBadge.tsx
│   └── DocumentPrintView.tsx
├── hooks/
│   ├── useDocuments.ts      ← TanStack Query hooks
│   └── useDocumentForm.ts
└── pages/
    ├── DocumentsPage.tsx
    ├── DocumentDetailPage.tsx
    └── DocumentCreatePage.tsx
```

### Gestion de l'authentification

```typescript
// stores/authStore.ts
interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials) => Promise<void>;
  logout: () => Promise<void>;
}
```

- Sanctum envoie un cookie `httpOnly` après `/api/v1/auth/login`
- Axios est configuré avec `withCredentials: true`
- Le store Zustand garde l'état local de l'utilisateur courant
- Route guard via React Router (`<RequireAuth>` wrapper)

### Build et déploiement

```bash
# Frontend
cd frontend
npm install
npm run build          # Produit frontend/dist/
# Copier dist/* dans backend/public/dist/ ou à la racine public/

# vite.config.ts — base URL
export default defineConfig({
  base: '/',
  build: {
    outDir: '../backend/public',   // Build directement dans public Laravel
    emptyOutDir: false,
  }
})
```

---

## 4. BACKEND — LARAVEL 12 API

### Stack technique

| Outil | Rôle |
|---|---|
| Laravel 12 | Framework PHP |
| Laravel Sanctum | Auth SPA (cookies httpOnly) |
| Laravel API Resources | Sérialisation JSON |
| Laravel Form Requests | Validation centralisée |
| Laravel Policies | Autorisations granulaires |
| DomPDF (barryvdh/laravel-dompdf) | Génération PDF sans Chrome |
| Laravel Notifications | Email + in-app |
| Laravel Schedule | Tâches cron (relances email) |

### Structure des routes API

```php
// routes/api.php
Route::prefix('v1')->group(function () {

    // Auth (pas de middleware auth)
    Route::post('/auth/login',   [AuthController::class, 'login']);
    Route::post('/auth/logout',  [AuthController::class, 'logout']);
    Route::get('/auth/me',       [AuthController::class, 'me'])->middleware('auth:sanctum');
    Route::post('/auth/password',[AuthController::class, 'changePassword'])->middleware('auth:sanctum');

    // Zone protégée
    Route::middleware('auth:sanctum')->group(function () {

        Route::get('/dashboard', [DashboardController::class, 'index']);

        Route::apiResource('clients',   ClientController::class);
        Route::get('/clients/{client}/follow-ups', ...);
        Route::post('/clients/{client}/follow-ups', ...);

        Route::get('/products/search',  [ProductController::class, 'search']);
        Route::apiResource('products',  ProductController::class);

        Route::get('/categories',       [CategoryController::class, 'index']);
        Route::apiResource('categories', CategoryController::class);

        Route::get('/suppliers',        [SupplierController::class, 'index']);
        Route::apiResource('suppliers', SupplierController::class);

        Route::get('/stock',            [StockController::class, 'index']);
        Route::post('/stock',           [StockController::class, 'store']);

        Route::apiResource('sales',     SaleController::class);
        Route::post('/sales/{sale}/cancel', [SaleController::class, 'cancel']);

        Route::apiResource('documents', DocumentController::class);
        Route::post('/documents/{document}/status',           [DocumentController::class, 'updateStatus']);
        Route::post('/documents/{document}/generate-bl',      [DocumentController::class, 'generateBl']);
        Route::post('/documents/{document}/generate-invoice', [DocumentController::class, 'generateInvoice']);
        Route::get('/documents/{document}/pdf',               [DocumentController::class, 'pdf']);
        Route::post('/documents/{document}/deletion/request', [DocumentController::class, 'requestDeletion']);
        Route::post('/documents/{document}/deletion/approve', [DocumentController::class, 'approveDeletion']);
        Route::post('/documents/{document}/deletion/reject',  [DocumentController::class, 'rejectDeletion']);

        Route::apiResource('cash-vouchers', CashVoucherController::class);
        Route::post('/cash-vouchers/{cv}/payment',        [CashVoucherController::class, 'recordPayment']);
        Route::post('/cash-vouchers/{cv}/confirm-receipt',[CashVoucherController::class, 'confirmReceipt']);
        Route::post('/cash-vouchers/{cv}/cancel',         [CashVoucherController::class, 'cancel']);
        Route::get('/cash-vouchers/{cv}/pdf',             [CashVoucherController::class, 'pdf']);

        Route::apiResource('permissions', PermissionController::class)->except(['edit','update']);
        Route::post('/permissions/{permission}/approve', [PermissionController::class, 'approve']);
        Route::post('/permissions/{permission}/refuse',  [PermissionController::class, 'refuse']);
        Route::get('/permissions/{permission}/pdf',      [PermissionController::class, 'pdf']);

        Route::prefix('ged')->name('ged.')->group(function () {
            Route::get('/',                          [GedController::class, 'index']);
            Route::post('/upload',                   [GedController::class, 'upload']);
            Route::post('/folders',                  [GedController::class, 'createFolder']);
            Route::get('/{file}',                    [GedController::class, 'show']);
            Route::put('/{file}',                    [GedController::class, 'update']);
            Route::delete('/{file}',                 [GedController::class, 'destroy']);
            Route::post('/{file}/restore',           [GedController::class, 'restore']);
            Route::get('/{file}/download',           [GedController::class, 'download']);
            Route::delete('/folders/{folder}',       [GedController::class, 'destroyFolder']);
        });

        // Admin uniquement
        Route::middleware('role:super_admin,admin')->group(function () {
            Route::apiResource('users', UserController::class);
            Route::put('/users/{user}/password', [UserController::class, 'updatePassword']);
        });

        Route::get('/notifications',           [NotificationController::class, 'index']);
        Route::post('/notifications/mark-read',[NotificationController::class, 'markRead']);
    });
});
```

### Format de réponse JSON cohérent

```json
// Succès (liste)
{
  "data": [...],
  "meta": { "current_page": 1, "last_page": 5, "total": 98, "per_page": 20 }
}

// Succès (objet)
{
  "data": { "id": 1, "name": "..." }
}

// Erreur de validation
{
  "message": "Les données fournies sont invalides.",
  "errors": { "email": ["L'email est requis."] }
}

// Erreur applicative
{
  "message": "Stock insuffisant pour ce produit.",
  "code": "INSUFFICIENT_STOCK"
}
```

### Exemple d'API Resource

```php
// app/Http/Resources/Api/V1/DocumentResource.php
class DocumentResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'                => $this->id,
            'reference'         => $this->reference,
            'type_code'         => $this->type_code,
            'type_label'        => $this->documentType?->label,
            'title'             => $this->title,
            'status'            => $this->status,
            'status_label'      => $this->status_label,
            'available_statuses'=> $this->statuts_disponibles(),
            'client'            => [
                'id'      => $this->client_id,
                'name'    => $this->client_nom,
                'address' => $this->client_adresse,
                'phone'   => $this->client_telephone,
                'email'   => $this->client_email,
                'rc'      => $this->client_rc,
                'niu'     => $this->client_niu,
            ],
            'amounts'           => [
                'subtotal'      => (float) $this->sous_total,
                'discount_pct'  => (float) $this->remise_globale,
                'discount_amt'  => (float) $this->remise_montant,
                'tax_rate'      => (float) $this->tva_taux,
                'tax_amount'    => (float) $this->tva_montant,
                'total'         => (float) $this->total_ttc,
                'total_words'   => $this->total_en_lettres,
            ],
            'dates'             => [
                'document'  => $this->date_document?->toDateString(),
                'validity'  => $this->date_validite?->toDateString(),
                'delivery'  => $this->date_livraison?->toDateString(),
            ],
            'lines'             => DocumentLineResource::collection($this->whenLoaded('lines')),
            'history'           => DocumentHistoryResource::collection($this->whenLoaded('historique')),
            'parent_id'         => $this->document_parent_id,
            'parent_ref'        => $this->document_parent_ref,
            'sale_id'           => $this->sale_id,
            'pending_deletion'  => $this->pending_deletion,
            'created_by'        => $this->creator?->name,
            'created_at'        => $this->created_at->toISOString(),
            'updated_at'        => $this->updated_at->toISOString(),
        ];
    }
}
```

### Policies (autorisations)

```php
// app/Policies/DocumentPolicy.php
class DocumentPolicy
{
    public function viewAny(User $user): bool   { return true; }
    public function view(User $user): bool      { return true; }
    public function create(User $user): bool    { return $user->isCommercial(); }
    public function update(User $user, Document $doc): bool {
        return $user->isCommercial() && !in_array($doc->status, ['VALIDE', 'LIVRE', 'FACTURE']);
    }
    public function delete(User $user, Document $doc): bool {
        return $user->isAdmin();
    }
    public function approveDeletion(User $user): bool {
        return $user->isAdmin();
    }
}
```

### Gestion centralisée des erreurs

```php
// app/Exceptions/Handler.php
public function register(): void
{
    $this->renderable(function (\Illuminate\Validation\ValidationException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json([
                'message' => 'Les données fournies sont invalides.',
                'errors'  => $e->errors(),
            ], 422);
        }
    });

    $this->renderable(function (\Illuminate\Auth\AuthenticationException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Non authentifié.'], 401);
        }
    });

    $this->renderable(function (\Illuminate\Auth\Access\AuthorizationException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => 'Accès refusé.'], 403);
        }
    });

    $this->renderable(function (\RuntimeException $e, $request) {
        if ($request->expectsJson()) {
            return response()->json(['message' => $e->getMessage()], 422);
        }
    });
}
```

---

## 5. AUTHENTIFICATION — LARAVEL SANCTUM SPA

### Pourquoi Sanctum (et pas JWT)

- OVH mutualisé : Sanctum fonctionne avec les cookies de session PHP standard
- Même domaine React + Laravel → cookies `httpOnly`, pas de tokens dans le localStorage (plus sécurisé)
- Laravel Sanctum est inclus dans Laravel 12 sans dépendance supplémentaire lourde
- Les CSRF tokens fonctionnent naturellement avec Axios

### Flow d'authentification

```
1. React → GET  /sanctum/csrf-cookie     (obtient le cookie CSRF)
2. React → POST /api/v1/auth/login       (envoie email + password)
3. Laravel valide, crée une session, retourne { user, permissions }
4. Axios envoie automatiquement le cookie de session sur chaque requête
5. React → GET  /api/v1/auth/me          (vérifie l'état de session)
6. React → POST /api/v1/auth/logout      (détruit la session)
```

### Configuration Sanctum

```php
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'gst.icavi-secure.com')),

// config/session.php
'domain'   => env('SESSION_DOMAIN', '.icavi-secure.com'),
'secure'   => true,
'same_site'=> 'lax',
```

```javascript
// frontend/src/lib/axios.ts
import axios from 'axios';

const api = axios.create({
  baseURL: '/api/v1',
  withCredentials: true,    // Envoie les cookies de session
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
});

// Intercepteur : rediriger vers /login sur 401
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export default api;
```

---

## 6. GESTION DES PERMISSIONS

### Système actuel → nouveau système

| Actuel | Nouveau |
|---|---|
| Middleware `CheckRole` sur routes | Laravel Policies + middleware |
| Rôles hardcodés dans les routes | Policy par ressource |
| Pas de granularité fine | `can('update', $document)` |

### Rôles conservés

```php
const ROLES = ['super_admin', 'admin', 'commercial', 'storage_manager'];
```

### Exposition des permissions à React

```php
// AuthController::me()
return [
    'user' => [
        'id'          => $user->id,
        'name'        => $user->name,
        'email'       => $user->email,
        'role'        => $user->role,
        'is_directeur'=> $user->is_directeur,
    ],
    'abilities' => [
        'can_manage_users'     => $user->isAdmin(),
        'can_create_documents' => $user->isCommercial(),
        'can_manage_stock'     => in_array($user->role, ['super_admin','admin','storage_manager']),
        'can_approve_permissions' => $user->isDirecteur(),
        'can_sign_cash_vouchers'  => $user->id === config('cash_vouchers.signer_user_id'),
    ],
];
```

```typescript
// hooks/useAbilities.ts
const { abilities } = useAuthStore();
const canCreateDocument = abilities?.can_create_documents ?? false;
```

---

## 7. BASE DE DONNÉES — STRATÉGIE

### Tables à créer (nouvelles)

```sql
-- Normalisation des catégories produits
CREATE TABLE categories (
  id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(100) NOT NULL UNIQUE,
  color      VARCHAR(7) DEFAULT '#6b7280',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Normalisation des fournisseurs
CREATE TABLE suppliers (
  id         BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(255) NOT NULL,
  email      VARCHAR(150),
  phone      VARCHAR(50),
  city       VARCHAR(100),
  rc         VARCHAR(50),
  niu        VARCHAR(50),
  status     ENUM('active','inactive') DEFAULT 'active',
  notes      TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```

### Migrations de données (produits)

```php
// Après création de la table categories :
// 1. Insérer toutes les catégories distinctes de products.category
// 2. Ajouter products.category_id
// 3. Remplir products.category_id depuis products.category
// 4. Garder products.category temporairement (rollback possible)
// 5. Supprimer products.category après validation
```

### Tables existantes — aucune modification destructive

Toutes les tables existantes sont conservées telles quelles. Les migrations ajoutent des colonnes ou des tables, ne modifient pas la structure existante sans stratégie de rollback.

---

## 8. GÉNÉRATION PDF — REMPLACEMENT DE CHROME

### Problème actuel
Chrome/Chromium headless est utilisé pour les bons de caisse. Non disponible sur OVH mutualisé.

### Solution : barryvdh/laravel-dompdf

```bash
composer require barryvdh/laravel-dompdf
```

```php
// Exemple : génération PDF d'un document
use Barryvdh\DomPDF\Facade\Pdf;

public function pdf(Document $document): \Illuminate\Http\Response
{
    $this->authorize('view', $document);
    $document->load('lines', 'creator');

    $pdf = Pdf::loadView('print.document', compact('document'))
        ->setPaper('A4', 'portrait')
        ->setOption('dpi', 150)
        ->setOption('defaultFont', 'DejaVu Sans');  // Pour caractères spéciaux

    return $pdf->download("document-{$document->reference}.pdf");
}
```

**Vues d'impression** : les vues Blade existantes (`documents/print.blade.php`, etc.) sont adaptées pour DomPDF (CSS inline, pas de CSS externe, pas de JS).

---

## 9. PERFORMANCE

### Backend (Laravel)

```php
// Eager loading systématique (éviter N+1)
Document::with('client', 'lines', 'creator', 'documentType')->paginate(20);

// Index SQL déjà présents sur :
// documents : type_code, status, client_id, date_document
// À ajouter : products.category_id, stock_movements.product_id, stock_movements.created_at

// Cache des statistiques dashboard (TTL court)
Cache::remember('dashboard.stats', 300, fn() => $this->computeStats());

// Pagination serveur systématique : per_page=20 par défaut
```

### Frontend (React)

```typescript
// TanStack Query : cache et revalidation
const { data: documents } = useQuery({
  queryKey: ['documents', filters],
  queryFn: () => documentApi.list(filters),
  staleTime: 30_000,          // 30s avant revalidation
  gcTime: 5 * 60 * 1000,      // 5 min en mémoire
});

// Pagination serveur (pas de chargement de toute la liste)
// Lazy loading des pages lourdes
const DocumentsPage = lazy(() => import('./features/documents/pages/DocumentsPage'));

// Debounce sur la recherche
const debouncedSearch = useDebounce(searchTerm, 300);
```

---

## 10. DÉPLOIEMENT OVH MUTUALISÉ PRO

### Structure sur OVH

```
/home/icavi/         (racine FTP)
├── backend/         ← Projet Laravel (hors public/)
│   ├── app/
│   ├── config/
│   ├── database/
│   ├── routes/
│   ├── storage/
│   └── vendor/
└── www/             ← Document root (pointe vers ici)
    ├── index.php    ← Laravel public/index.php (copié ou symlink)
    ├── .htaccess    ← Laravel + SPA routing
    ├── dist/        ← Build React (assets statiques)
    └── storage/     ← Symlink vers ../backend/storage/app/public
```

### `.htaccess` (public/)

```apache
Options -MultiViews -Indexes

RewriteEngine On

# API → Laravel
RewriteCond %{REQUEST_URI} ^/api/ [NC]
RewriteRule ^ index.php [L]

# Sanctum CSRF
RewriteCond %{REQUEST_URI} ^/sanctum/ [NC]
RewriteRule ^ index.php [L]

# Fichiers statiques React (dist/)
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]

# SPA fallback → React
RewriteCond %{REQUEST_URI} !^/api/
RewriteCond %{REQUEST_URI} !^/sanctum/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]
```

### Commandes de déploiement

```bash
# 1. Backend
composer install --no-dev --optimize-autoloader --no-interaction
php artisan key:generate --force
php artisan migrate --force
php artisan storage:link
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize

# 2. Frontend
cd frontend
npm ci
npm run build
# Build produit dans backend/public/ directement

# 3. Permissions (OVH)
chmod -R 755 storage/ bootstrap/cache/
```

### Variables d'environnement (.env production)

```env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://gst.icavi-secure.com

SESSION_DOMAIN=.icavi-secure.com
SANCTUM_STATEFUL_DOMAINS=gst.icavi-secure.com
SESSION_DRIVER=file
SESSION_SECURE_COOKIE=true

CACHE_STORE=file
QUEUE_CONNECTION=sync

FILESYSTEM_DISK=local
```

### Cron OVH (relances email)

OVH mutualisé Pro supporte les tâches cron planifiées.
Configurer dans l'interface OVH :
```
php /home/icavi/backend/artisan schedule:run >> /dev/null 2>&1
```
Intervalle : toutes les heures (minimum supporté sur mutualisé).

> **Note :** Si OVH refuse la commande directe `php artisan`, utiliser le wrapper :
> ```bash
> /usr/local/php8.2/bin/php /home/icavi/backend/artisan schedule:run
> ```

---

## 11. CORS

La configuration CORS est cruciale si un jour React et Laravel sont sur des domaines différents.

```php
// config/cors.php
return [
    'paths'             => ['api/*', 'sanctum/csrf-cookie'],
    'allowed_origins'   => [env('FRONTEND_URL', 'https://gst.icavi-secure.com')],
    'allowed_methods'   => ['*'],
    'allowed_headers'   => ['*'],
    'supports_credentials' => true,   // Obligatoire pour Sanctum SPA
];
```

---

## 12. ENVIRONNEMENTS

| Environnement | URL | Laravel | React |
|---|---|---|---|
| **Local dev** | localhost:5173 | `php artisan serve` (8000) | `npm run dev` (5173) |
| **Production** | gst.icavi-secure.com | OVH PHP | fichiers statiques dans public/ |

En développement local, Vite proxy les requêtes `/api/*` vers Laravel :

```typescript
// vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': { target: 'http://localhost:8000', changeOrigin: true },
      '/sanctum': { target: 'http://localhost:8000', changeOrigin: true },
    }
  },
  build: {
    outDir: '../backend/public',
    emptyOutDir: false,
  }
});
```

---

## 13. SÉCURITÉ

| Point | Mesure |
|---|---|
| Auth | Sanctum + cookies httpOnly + SameSite=Lax |
| CSRF | Token Sanctum sur chaque requête state-changing |
| SQL Injection | Eloquent ORM, jamais de requêtes raw non paramétrées |
| Mass assignment | `$fillable` strict sur tous les modèles |
| XSS | React échappe nativement, DomPDF échappe les templates Blade |
| Rate limiting | `throttle:6,1` sur `/api/v1/auth/login` |
| Auth frontend | Vérification côté Laravel uniquement (React = présentation) |
| Upload fichiers | Validation type MIME + taille dans GedController |
| Logs | `LOG_LEVEL=error` en production |
| APP_DEBUG | `false` obligatoire en production |
| Accès storage | Seul `public/storage` exposé, le reste hors webroot |
