# api/ — Laravel REST API

Ce fichier complète le CLAUDE.md racine. Stack : Laravel 12, PHP 8.3+,
PostgreSQL, Redis.

## Commandes

```bash
composer install
cp .env.example .env && php artisan key:generate
php artisan migrate --seed
php artisan serve              # dev
php artisan test               # tests
php artisan horizon            # queues (paiement, SMS)
```

## Stack & packages clés

- **Auth** : Laravel Sanctum (tokens). Trois rôles via
  `spatie/laravel-permission` : `traveler`, `agent`, `company_admin`.
- **Queues** : Redis + Horizon. Tout webhook de paiement, envoi SMS, et job
  de réconciliation passe par une queue — jamais de traitement synchrone
  dans le contrôleur du webhook.
- **QR** : `simplesoftwareio/simple-qrcode`.
- **Audit** : `spatie/laravel-activitylog` sur les modèles Booking et
  Payment.

## Conventions de code

- Chaque route publique passe par un **Form Request** dédié (validation)
  et retourne une **API Resource** (jamais un modèle Eloquent brut en
  JSON).
- Logique métier dans des **Services** (`app/Services/`), pas dans les
  contrôleurs. Ex. `BookingService`, `PaymentService`, `SeatHoldService`.
- Un contrôleur = orchestration uniquement (valider → appeler le service →
  retourner la resource).
- Tests : Feature tests sur chaque endpoint (au minimum le succès + un cas
  d'erreur de validation + un cas d'autorisation refusée).

## Règles spécifiques au domaine

### Sièges & holds (anti-survente)
- Chaque `Departure` a `app_seats_quota` et `app_seats_available`.
- Poser un hold = transaction DB avec `lockForUpdate()` sur la ligne
  Departure, décrément de `app_seats_available`, création d'un `Booking`
  en statut `pending` avec `reserved_until = now()->addMinutes(10)`.
- Un job planifié (`ExpireStaleBookings`, toutes les minutes) repasse les
  Bookings `pending` expirés en `expired` et restitue le quota.
- Ne JAMAIS décrémenter `app_seats_available` hors transaction verrouillée.

### Paiement
- Un seul agrégateur intégré au départ (CinetPay ou Hub2 — voir
  `app/Services/PaymentService.php` une fois créé). Toute la logique
  spécifique au provider reste encapsulée dans ce service, jamais dans le
  contrôleur ni dans un job.
- **Webhook** : vérifier la signature HMAC avant tout traitement. Si la
  signature est invalide → 401, ne rien faire d'autre.
- **Idempotence** : contrainte `UNIQUE` sur `payments.provider_tx_id`.
  Utiliser `firstOrCreate`/`updateOrCreate` sur cette colonne, jamais de
  `create()` simple dans le handler de webhook.
- **Montant** : toujours recalculé côté serveur à partir du Booking, ne
  jamais faire confiance au montant transmis par le client ou même par le
  payload du webhook sans le comparer au montant attendu.
- Un `Booking` passe à `paid` UNIQUEMENT depuis le webhook confirmé, jamais
  depuis la réponse synchrone d'initiation de paiement.
- Job quotidien `ReconcilePayments` : compare les transactions de
  l'agrégateur (API de listing) aux `Payment` en base, log les écarts.

### Tickets
- Un `Ticket` est généré (QR + code à 6 chiffres) uniquement quand le
  `Booking` passe à `paid`, jamais avant.
- Le code à 6 chiffres est le repli SMS quand l'utilisateur n'a pas de
  data — il doit être lisible/dictable à voix haute (pas de caractères
  ambigus type 0/O, 1/I).

## Contrat d'API

Après tout ajout/modification de route, mets à jour
`../contracts/openapi.yaml` pour qu'il corresponde exactement aux champs
réels retournés par l'API Resource.
