# ALDANA Phase 4 — Booking core Implementation Plan

**Goal:** Implémenter le flux de réservation de bout en bout (créer + annuler) avec transaction SQL atomique garantissant 0 surbooking et 0 séance perdue en cas de concurrence. Politique d'annulation J-24h appliquée côté front et serveur. Email de confirmation envoyé (logué en dev). Tests Feature PHPUnit sur les cas critiques.

**Architecture:**
- `BookingService` orchestre la transaction (FOR UPDATE sur t_classes + t_subscriptions, décrément atomique, INSERT t_bookings).
- `BookingRepository` + `SubscriptionRepository` pour les accès SQL préparés.
- `BookingController` gère POST /api/booking/create et /cancel.
- Frontend : panel slide-in sur /planning (JS vanilla minimal) + vue /mon-compte/reservations enrichie.
- Données de test : script `database/seed_dev.php` (15 cours + 1 user client + 1 forfait actif).

**Tech Stack:** PHP 8.4 PDO `SELECT ... FOR UPDATE`, transactions InnoDB, PHPUnit 10 sur BDD `aldanadb_test` séparée.

**Pré-requis :** P0-P3 livrés. WAMP actif. `aldanadb` peuplée.

---

## Transaction SQL critique (spec §5.2)

```sql
START TRANSACTION;

-- 1) Verrouiller la rangée du cours
SELECT spots_left FROM t_classes WHERE class_ID = ? AND status = 'scheduled' FOR UPDATE;
-- IF aucune ligne → ROLLBACK + 'class_not_found'
-- IF spots_left <= 0 → ROLLBACK + 'class_full'

-- 2) Verrouiller le forfait actif du membre
SELECT subscription_ID, sessions_left, valid_until
FROM t_subscriptions
WHERE user_ID = ? AND status = 'active'
  AND valid_until >= :class_date
  AND (sessions_left IS NULL OR sessions_left > 0)
ORDER BY valid_until ASC
LIMIT 1
FOR UPDATE;
-- IF aucune ligne → ROLLBACK + 'no_active_subscription'

-- 3) Décrémenter spots_left
UPDATE t_classes SET spots_left = spots_left - 1
WHERE class_ID = ? AND spots_left > 0;
-- IF ROW_COUNT() != 1 → ROLLBACK + 'concurrent_booking'

-- 4) Décrémenter sessions si applicable (NULL = illimité)
UPDATE t_subscriptions
SET sessions_left = sessions_left - 1
WHERE subscription_ID = ? AND sessions_left IS NOT NULL AND sessions_left > 0;

-- 5) Créer la réservation
INSERT INTO t_bookings (class_ID, user_ID, subscription_ID, status)
VALUES (?, ?, ?, 'confirmed');

COMMIT;
```

→ Garantit **0 surbooking** et **0 séance décomptée sans réservation** même sous concurrence.

---

## File Structure

```
src/
├── Controllers/
│   └── BookingController.php       ← Task 9 (POST create + cancel)
├── Repositories/
│   ├── BookingRepository.php       ← Task 3 (create, findByID, cancel, listForUser, FOR UPDATE)
│   └── SubscriptionRepository.php  ← Task 2 (findActiveForUser FOR UPDATE)
├── Services/
│   └── BookingService.php          ← Task 4 (createBooking + cancelBooking transactions)
├── Exceptions/
│   └── BookingException.php        ← Task 4
└── (EmailService.php MODIF — Task 5)

views/
├── pages/
│   ├── account/
│   │   ├── dashboard.php           ← Task 13 (vraie data : prochaine résa + forfait)
│   │   └── reservations.php        ← Task 13 (listing + cancel)
│   └── public/
│       └── planning.php            ← Task 12 (intégration booking panel + JS)
└── partials/
    └── booking-panel.php           ← Task 12 (slide-in)

public/assets/
├── css/aldana.css                  ← Task 12 (styles booking-panel)
└── js/aldana.js                    ← Task 12 (ouverture panel + POST AJAX)

database/
├── seed_dev.php                    ← Task 1 (15 cours + test user + forfait)
└── (existing migrations untouched)

tests/
├── bootstrap.php                   ← Task 7
└── Feature/
    └── BookingServiceTest.php      ← Task 8 (5+ test cases)

config/
└── routes.php                      ← MODIF Task 10 (POST /api/booking/* + auth required)

phpunit.xml                         ← Task 7
```

---

## Liste des tâches (16 au total)

| # | Tâche |
|---|---|
| 1 | `database/seed_dev.php` (cours + test user + forfait) |
| 2 | `SubscriptionRepository` (findActiveForUser FOR UPDATE, decrementSessions, restoreSession) |
| 3 | `BookingRepository` (create, findByIDForUser, listForUser, cancel) + `ClassRepository::findByIDForUpdate` |
| 4 | `BookingService` + `BookingException` (createBooking + cancelBooking en transaction) |
| 5 | `EmailService` extension (sendBookingConfirmation + sendCancellationConfirmation) |
| 6 | Setup PHPUnit (`phpunit.xml`, `tests/bootstrap.php`, BDD test `aldanadb_test`) |
| 7 | `tests/Feature/BookingServiceTest.php` (succès, class_full, no_subscription, J-24h, double booking) |
| 8 | Exécution des tests, validation verte |
| 9 | `BookingController` (createBooking, cancelBooking) |
| 10 | `routes.php` : POST /api/booking/create + cancel (auth requise) |
| 11 | Vue `planning.php` : activer booking panel (bouton sur chaque cours) |
| 12 | Partial `booking-panel.php` + CSS slide-in + JS handler (fetch POST + UI) |
| 13 | Vue `account/reservations.php` enrichie (listing + cancel button) + `dashboard.php` (prochaine résa + forfait actif) |
| 14 | Smoke test end-to-end (curl : login → réserver → annuler → vérifier compteurs) |
| 15 | Commit P4 |
| 16 | (bonus) Lister Aurane comme ayant un forfait pour pouvoir tester en tant que membre |

---

## Self-Review

- [ ] Transaction SQL utilise FOR UPDATE sur t_classes ET t_subscriptions (verrouillage des deux côtés)
- [ ] ROW_COUNT() == 1 vérifié après UPDATE spots_left (sinon ROLLBACK)
- [ ] sessions_left NULL = illimité (abo mensuel) — non décrémenté
- [ ] valid_until >= class_date vérifié (pas de booking sur cours après expiration forfait)
- [ ] J-24h vérifié serveur AVANT décrément (pas que côté JS)
- [ ] UNIQUE KEY uq_class_user_confirmed empêche le double booking au niveau SQL
- [ ] Auth middleware sur /api/booking/* (utilisateur identifié obligatoire)
- [ ] CSRF token sur les POST (helper csrf_field utilisé par le JS qui lit le token)
- [ ] Email confirmation envoyé après COMMIT (jamais avant — si email plante, la résa reste OK)
- [ ] Tests Feature couvrent : succès, complet, pas de forfait, expiration forfait, J-24h cancel

---

**Durée estimée** : 3-4 jours dev humain · ~1h exécution inline.

**Prochain plan** : P5 Stripe (paiement + webhooks).
