# ALDANA Phase 1 — Migrations BDD + Seeds Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Note pour P1 :** comme P0, les tâches sont quasi-toutes des `Write` de fichiers SQL/PHP scaffolding mécanique. Exécution **inline** plus efficace que subagent-driven (le décision tree de la skill subagent-driven elle-même renvoie vers "Manual" pour tâches tightly-coupled).

**Goal:** Créer les 12 tables de la base ALDANA via 11 migrations + 1 seed, avec helper migration runner idempotent, et alimentation initiale (Aurane admin, 2 pratiques Yoga/Power Yoga, 3 formules hypothèses, 10 settings clé-valeur).

**Architecture:** Pattern migration BookConnect — fichiers numérotés `001_*.sql` à `011_*.sql` exécutés dans l'ordre par un runner PHP. Pattern idempotent natif via `CREATE TABLE IF NOT EXISTS`. Le runner trace les migrations appliquées dans une table `t_migrations` pour ne jamais rejouer une migration déjà OK. Le seed `012_seed_initial_data.sql` est jouable à part (modifie data, pas schéma).

**Tech Stack:** MySQL 9.1.0 (WAMP local), PHP 8.4 (WAMP), PDO via `App\Helpers\Database`, conventions ALDANA (tables `t_*`, snake_case, `*_ID` PK INT UNSIGNED, utf8mb4_unicode_ci, InnoDB).

**Pré-requis :**

- P0 livrée (commit `d99a32b`) : composer install OK, BDD `aldana` créée vide, Database singleton fonctionnel
- WAMP actif (PHP 8.4 + MySQL 9.1)
- Working directory : `C:\Users\rgalo\Dropbox\_ALDANA\`

---

## File Structure

```
_ALDANA/
└── database/
    ├── migrate.php                         ← Task 1 (runner)
    ├── README.md                           ← Task 16
    ├── migrations/
    │   ├── 001_create_users.sql            ← Task 2
    │   ├── 002_create_practices.sql        ← Task 3
    │   ├── 003_create_classes.sql          ← Task 4
    │   ├── 004_create_subscription_plans.sql  ← Task 5
    │   ├── 005_create_subscriptions.sql    ← Task 6
    │   ├── 006_create_bookings.sql         ← Task 7
    │   ├── 007_create_events.sql           ← Task 8
    │   ├── 008_create_payments.sql         ← Task 9
    │   ├── 009_create_event_bookings.sql   ← Task 10
    │   ├── 010_create_private_requests.sql ← Task 11
    │   ├── 011_create_processed_webhooks.sql ← Task 12
    │   ├── 012_create_site_settings.sql    ← Task 13
    │   └── 013_seed_initial_data.sql       ← Task 14
    └── seeds/ (vide, P5+ pour seeds d'env staging)
```

**Ordre d'exécution des migrations** (déterminé par les contraintes FK) :

```
users  practices  subscription_plans  events  processed_webhooks  site_settings   (sans FK)
   ↓        ↓             ↓
   ↓     classes          ↓
   ↓                      ↓
   └→ subscriptions ←─────┘
            ↓
        bookings  ←─ classes
            ↓
        payments  ←─ users, subscriptions
            ↓
     event_bookings  ←─ events, users, payments
            ↓
     private_requests ←─ users
```

---

## Task 1: Migration runner `database/migrate.php`

**Files:**

- Create: `database/migrate.php`

Le runner :
1. Lit tous les fichiers `database/migrations/*.sql` triés par nom
2. Vérifie quelles migrations sont déjà appliquées dans `t_migrations` (créée auto au 1er run)
3. Joue celles qui manquent, transactionnellement
4. Trace chaque migration appliquée

- [ ] **Step 1: Écrire le runner**

```php
<?php
declare(strict_types=1);

/**
 * Migration runner ALDANA.
 *
 * Usage :
 *   php database/migrate.php           # Joue les migrations en attente
 *   php database/migrate.php --status  # Affiche l'état (joué/en attente)
 *   php database/migrate.php --redo    # Force le replay de la dernière (debug)
 *
 * Idempotent : ne rejoue jamais une migration déjà appliquée.
 */

require __DIR__ . '/../vendor/autoload.php';

\Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->safeLoad();
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Paris');

$pdo = \App\Helpers\Database::getInstance()->getConnection();

// Table de tracking des migrations
$pdo->exec(<<<SQL
    CREATE TABLE IF NOT EXISTS t_migrations (
        migration_ID    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        filename        VARCHAR(200) NOT NULL UNIQUE,
        applied_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_applied (applied_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL);

$dir = __DIR__ . '/migrations';
$files = glob($dir . '/*.sql');
sort($files);

$applied = $pdo->query('SELECT filename FROM t_migrations')
    ->fetchAll(\PDO::FETCH_COLUMN);
$applied = array_flip($applied);

$mode = $argv[1] ?? '';

if ($mode === '--status') {
    echo "État des migrations ALDANA\n";
    echo str_repeat('-', 60) . "\n";
    foreach ($files as $file) {
        $name = basename($file);
        $status = isset($applied[$name]) ? '✓ appliquée' : '… en attente';
        echo sprintf("%-50s %s\n", $name, $status);
    }
    exit(0);
}

if ($mode === '--redo') {
    if (empty($applied)) {
        echo "Aucune migration appliquée à rejouer.\n";
        exit(1);
    }
    $last = array_key_last($applied);
    $pdo->prepare('DELETE FROM t_migrations WHERE filename = ?')->execute([$last]);
    unset($applied[$last]);
    echo "Migration $last marquée à rejouer.\n";
}

$pending = array_filter($files, fn($f) => !isset($applied[basename($f)]));

if (empty($pending)) {
    echo "Aucune migration en attente. Base à jour.\n";
    exit(0);
}

echo "Migrations à jouer : " . count($pending) . "\n";

foreach ($pending as $file) {
    $name = basename($file);
    echo "→ $name ... ";

    $sql = file_get_contents($file);
    if ($sql === false || trim($sql) === '') {
        echo "ÉCHEC (fichier vide ou illisible)\n";
        exit(1);
    }

    try {
        $pdo->beginTransaction();

        // MySQL ne supporte pas plusieurs CREATE en transaction strict (DDL implicit commit)
        // mais on l'enroule quand même pour la table de tracking
        foreach (explode(';', $sql) as $stmt) {
            $stmt = trim($stmt);
            if ($stmt === '' || str_starts_with($stmt, '--')) {
                continue;
            }
            $pdo->exec($stmt);
        }

        $pdo->prepare('INSERT INTO t_migrations (filename) VALUES (?)')->execute([$name]);
        $pdo->commit();
        echo "OK\n";
    } catch (\Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        echo "ÉCHEC\n";
        echo "  " . $e->getMessage() . "\n";
        exit(1);
    }
}

echo "\nMigrations terminées avec succès.\n";
```

- [ ] **Step 2: Vérifier la syntaxe**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe -l database/migrate.php
```

Attendu : `No syntax errors detected in database/migrate.php`.

- [ ] **Step 3: Lancer le runner avec --status (aucune migration encore, juste créer t_migrations)**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php --status
```

Attendu : message « Aucune migration en attente » ou liste vide. Vérifier que `t_migrations` a été créée :

```powershell
C:\wamp64\bin\mysql\mysql9.1.0\bin\mysql.exe -u root aldana -e "SHOW TABLES;"
```

Attendu : `t_migrations` listée.

---

## Task 2: Migration 001 — `t_users`

**Files:**

- Create: `database/migrations/001_create_users.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 001
-- Table utilisateurs : auth + rôles (Admin Studio 29, Professeur 11, Client 10)

CREATE TABLE IF NOT EXISTS t_users (
    user_ID                     INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email                       VARCHAR(255) NOT NULL UNIQUE,
    password_hash               VARCHAR(255) NOT NULL,
    full_name                   VARCHAR(150) NOT NULL,
    phone                       VARCHAR(30) NULL,
    role_ID                     TINYINT UNSIGNED NOT NULL DEFAULT 10,
    email_verified_at           TIMESTAMP NULL DEFAULT NULL,
    email_verification_token    VARCHAR(64) NULL,
    password_reset_token        VARCHAR(64) NULL,
    password_reset_expires_at   DATETIME NULL,
    failed_login_attempts       TINYINT UNSIGNED NOT NULL DEFAULT 0,
    locked_until                DATETIME NULL,
    created_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_email (email),
    INDEX idx_role  (role_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 3: Migration 002 — `t_practices`

**Files:**

- Create: `database/migrations/002_create_practices.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 002
-- Pratiques proposées (Yoga, Power Yoga, ...)

CREATE TABLE IF NOT EXISTS t_practices (
    practice_ID         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name                VARCHAR(100) NOT NULL,
    slug                VARCHAR(100) NOT NULL UNIQUE,
    short_description   VARCHAR(200) NULL,
    description         TEXT NULL,
    intensity           TINYINT UNSIGNED NULL,
    pace                TINYINT UNSIGNED NULL,
    heat                TINYINT UNSIGNED NULL,
    duration_default_min SMALLINT UNSIGNED NULL,
    image_url           VARCHAR(500) NULL,
    display_order       SMALLINT UNSIGNED NOT NULL DEFAULT 100,
    active              TINYINT(1) NOT NULL DEFAULT 1,
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_active_order (active, display_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 4: Migration 003 — `t_classes`

**Files:**

- Create: `database/migrations/003_create_classes.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 003
-- Cours collectifs planifiés (FK → t_practices)

CREATE TABLE IF NOT EXISTS t_classes (
    class_ID        INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    practice_ID     INT UNSIGNED NOT NULL,
    title           VARCHAR(150) NOT NULL,
    class_date      DATE NOT NULL,
    class_time      TIME NOT NULL,
    duration_min    SMALLINT UNSIGNED NOT NULL,
    capacity        TINYINT UNSIGNED NOT NULL,
    spots_left      TINYINT UNSIGNED NOT NULL,
    status          ENUM('scheduled','cancelled','completed') NOT NULL DEFAULT 'scheduled',
    notes           TEXT NULL,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (practice_ID) REFERENCES t_practices(practice_ID) ON DELETE RESTRICT,
    INDEX idx_date (class_date),
    INDEX idx_practice_date (practice_ID, class_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 5: Migration 004 — `t_subscription_plans`

**Files:**

- Create: `database/migrations/004_create_subscription_plans.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 004
-- Formules tarifaires (unitaire, carnets, abonnements)

CREATE TABLE IF NOT EXISTS t_subscription_plans (
    plan_ID         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name            VARCHAR(100) NOT NULL,
    slug            VARCHAR(100) NOT NULL UNIQUE,
    type            ENUM('unit','pack','monthly_unlimited','annual_unlimited') NOT NULL,
    sessions_count  SMALLINT UNSIGNED NULL,
    validity_days   SMALLINT UNSIGNED NULL,
    price_cents     INT UNSIGNED NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'EUR',
    stripe_price_id VARCHAR(100) NULL,
    description     TEXT NULL,
    display_order   SMALLINT UNSIGNED NOT NULL DEFAULT 100,
    active          TINYINT(1) NOT NULL DEFAULT 1,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_active_order (active, display_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 6: Migration 005 — `t_subscriptions`

**Files:**

- Create: `database/migrations/005_create_subscriptions.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 005
-- Forfaits actifs par membre (FK → t_users, t_subscription_plans)

CREATE TABLE IF NOT EXISTS t_subscriptions (
    subscription_ID         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_ID                 INT UNSIGNED NOT NULL,
    plan_ID                 INT UNSIGNED NOT NULL,
    sessions_left           SMALLINT UNSIGNED NULL,
    valid_from              DATE NOT NULL,
    valid_until             DATE NOT NULL,
    status                  ENUM('active','expired','cancelled','refunded') NOT NULL DEFAULT 'active',
    stripe_subscription_id  VARCHAR(100) NULL,
    stripe_payment_intent_id VARCHAR(100) NULL,
    created_at              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at              TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_ID) REFERENCES t_users(user_ID) ON DELETE RESTRICT,
    FOREIGN KEY (plan_ID) REFERENCES t_subscription_plans(plan_ID) ON DELETE RESTRICT,
    INDEX idx_user_status (user_ID, status),
    INDEX idx_valid_until (valid_until)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 7: Migration 006 — `t_bookings`

**Files:**

- Create: `database/migrations/006_create_bookings.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 006
-- Réservations de cours collectifs (FK → t_classes, t_users, t_subscriptions)
-- subscription_ID NULLABLE pour cas exceptionnels (cours offert par Aurane)

CREATE TABLE IF NOT EXISTS t_bookings (
    booking_ID          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    class_ID            INT UNSIGNED NOT NULL,
    user_ID             INT UNSIGNED NOT NULL,
    subscription_ID     INT UNSIGNED NULL,
    status              ENUM('confirmed','cancelled','attended','no_show') NOT NULL DEFAULT 'confirmed',
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    cancelled_at        DATETIME NULL,
    attended_marked_at  DATETIME NULL,
    FOREIGN KEY (class_ID) REFERENCES t_classes(class_ID) ON DELETE RESTRICT,
    FOREIGN KEY (user_ID) REFERENCES t_users(user_ID) ON DELETE RESTRICT,
    FOREIGN KEY (subscription_ID) REFERENCES t_subscriptions(subscription_ID) ON DELETE RESTRICT,
    UNIQUE KEY uq_class_user_confirmed (class_ID, user_ID, status),
    INDEX idx_user_created (user_ID, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 8: Migration 007 — `t_events`

**Files:**

- Create: `database/migrations/007_create_events.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 007
-- Events (Week-Ends Bien-Être)

CREATE TABLE IF NOT EXISTS t_events (
    event_ID        INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    title           VARCHAR(200) NOT NULL,
    slug            VARCHAR(200) NOT NULL UNIQUE,
    subtitle        VARCHAR(200) NULL,
    date_start      DATETIME NOT NULL,
    date_end        DATETIME NOT NULL,
    location        VARCHAR(300) NULL,
    description     TEXT NULL,
    price_cents     INT UNSIGNED NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'EUR',
    stripe_price_id VARCHAR(100) NULL,
    capacity        SMALLINT UNSIGNED NOT NULL,
    spots_left      SMALLINT UNSIGNED NOT NULL,
    image_url       VARCHAR(500) NULL,
    status          ENUM('draft','published','sold_out','completed','cancelled') NOT NULL DEFAULT 'draft',
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_status_date (status, date_start)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 9: Migration 008 — `t_payments`

**Files:**

- Create: `database/migrations/008_create_payments.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 008
-- Historique paiements Stripe (FK → t_users, t_subscriptions, self-FK refund)
-- event_booking_ID référence t_event_bookings qui sera créée en migration 009
-- → on déclare la colonne ici mais pas la FK (ajoutée en 009 via ALTER)

CREATE TABLE IF NOT EXISTS t_payments (
    payment_ID                  INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_ID                     INT UNSIGNED NOT NULL,
    subscription_ID             INT UNSIGNED NULL,
    event_booking_ID            INT UNSIGNED NULL,
    amount_cents                INT UNSIGNED NOT NULL,
    currency                    CHAR(3) NOT NULL DEFAULT 'EUR',
    type                        ENUM('subscription_purchase','subscription_renewal','event_purchase','refund','adjustment') NOT NULL,
    stripe_payment_intent_id    VARCHAR(100) NULL,
    stripe_invoice_id           VARCHAR(100) NULL,
    stripe_charge_id            VARCHAR(100) NULL,
    invoice_pdf_url             VARCHAR(500) NULL,
    status                      ENUM('pending','succeeded','failed','refunded') NOT NULL DEFAULT 'pending',
    refunded_payment_ID         INT UNSIGNED NULL,
    created_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_ID) REFERENCES t_users(user_ID) ON DELETE RESTRICT,
    FOREIGN KEY (subscription_ID) REFERENCES t_subscriptions(subscription_ID) ON DELETE SET NULL,
    FOREIGN KEY (refunded_payment_ID) REFERENCES t_payments(payment_ID) ON DELETE SET NULL,
    INDEX idx_user_created (user_ID, created_at),
    INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 10: Migration 009 — `t_event_bookings`

**Files:**

- Create: `database/migrations/009_create_event_bookings.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 009
-- Réservations d'events + FK retardée t_payments.event_booking_ID

CREATE TABLE IF NOT EXISTS t_event_bookings (
    event_booking_ID    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    event_ID            INT UNSIGNED NOT NULL,
    user_ID             INT UNSIGNED NOT NULL,
    payment_ID          INT UNSIGNED NULL,
    status              ENUM('confirmed','cancelled','refunded') NOT NULL DEFAULT 'confirmed',
    notes               TEXT NULL,
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    cancelled_at        DATETIME NULL,
    FOREIGN KEY (event_ID) REFERENCES t_events(event_ID) ON DELETE RESTRICT,
    FOREIGN KEY (user_ID) REFERENCES t_users(user_ID) ON DELETE RESTRICT,
    FOREIGN KEY (payment_ID) REFERENCES t_payments(payment_ID) ON DELETE SET NULL,
    UNIQUE KEY uq_event_user_confirmed (event_ID, user_ID, status),
    INDEX idx_user (user_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

Note : la FK `t_payments.event_booking_ID → t_event_bookings.event_booking_ID` reste **non déclarée** dans le DDL. Ce n'est pas critique : la cohérence est garantie par la logique applicative (création groupée dans la même transaction P5).

---

## Task 11: Migration 010 — `t_private_requests`

**Files:**

- Create: `database/migrations/010_create_private_requests.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 010
-- Demandes de cours particuliers (user_ID nullable : formulaire ouvert visiteurs)

CREATE TABLE IF NOT EXISTS t_private_requests (
    request_ID      INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_ID         INT UNSIGNED NULL,
    full_name       VARCHAR(150) NOT NULL,
    email           VARCHAR(255) NOT NULL,
    phone           VARCHAR(30) NULL,
    preferred_dates TEXT NULL,
    message         TEXT NULL,
    status          ENUM('new','in_progress','scheduled','declined','closed') NOT NULL DEFAULT 'new',
    aurane_response TEXT NULL,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_ID) REFERENCES t_users(user_ID) ON DELETE SET NULL,
    INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 12: Migration 011 — `t_processed_webhooks`

**Files:**

- Create: `database/migrations/011_create_processed_webhooks.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 011
-- Idempotence des webhooks Stripe : un event_id Stripe = une seule fois traité

CREATE TABLE IF NOT EXISTS t_processed_webhooks (
    event_id        VARCHAR(100) PRIMARY KEY,
    event_type      VARCHAR(100) NOT NULL,
    processed_at    TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_type_date (event_type, processed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 13: Migration 012 — `t_site_settings`

**Files:**

- Create: `database/migrations/012_create_site_settings.sql`

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 012
-- CMS léger clé/valeur pour contenu éditable par Aurane via admin

CREATE TABLE IF NOT EXISTS t_site_settings (
    setting_key     VARCHAR(100) PRIMARY KEY,
    setting_value   TEXT NULL,
    setting_type    ENUM('text','html','image_url','json','int','bool') NOT NULL DEFAULT 'text',
    description     VARCHAR(300) NULL,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Task 14: Migration 013 — Seed initial

**Files:**

- Create: `database/migrations/013_seed_initial_data.sql`

Données initiales **hypothèses** à valider avec Aurane (cf spec §10) :

- **Aurane** : `aldana.aurane@hotmail.fr`, role Admin Studio (29). Password placeholder qu'elle redéfinira via /admin lors du go-live.
- **2 pratiques** : Yoga, Power Yoga
- **3 formules tarifaires** : Unitaire 25 €, Carnet 10 séances 180 €, Abo mensuel illimité 110 €/mois
- **Settings** : citation, infos lieu, fenêtre annulation, etc.

- [ ] **Step 1: Créer le fichier**

```sql
-- ALDANA migration 013 (seed)
-- Données initiales — valeurs hypothèses à valider avec Aurane.
-- Idempotent via INSERT IGNORE et ON DUPLICATE KEY UPDATE.

-- Aurane Aldana (Admin Studio = role_ID 29)
-- Le password_hash ci-dessous correspond au placeholder 'aldana-temp-2026'
-- (hashé avec PASSWORD_ARGON2ID). Aurane le change au 1er login.
INSERT INTO t_users (email, password_hash, full_name, role_ID, email_verified_at)
VALUES (
    'aldana.aurane@hotmail.fr',
    '$argon2id$v=19$m=65536,t=4,p=1$YWxkYW5hLXNhbHQtdGVtcA$bm9wZS1yZWdlbmVyYXRlLXRoaXM',
    'Aurane Aldana',
    29,
    CURRENT_TIMESTAMP
)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;

-- Pratiques
INSERT INTO t_practices (name, slug, short_description, description, intensity, pace, heat, duration_default_min, display_order)
VALUES
    ('Yoga', 'yoga',
     'L''invitation au silence intérieur',
     'Pratique posturale fluide et accessible, mêlant alignement et respiration consciente. Adapté à tous les niveaux.',
     2, 2, 1, 75, 10),
    ('Power Yoga', 'power-yoga',
     'La force dans le mouvement',
     'Pratique dynamique et exigeante, enchaînement soutenu, montée en chaleur. Pour qui cherche l''effort et la transformation.',
     4, 4, 3, 60, 20)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;

-- Formules tarifaires
INSERT INTO t_subscription_plans (name, slug, type, sessions_count, validity_days, price_cents, description, display_order)
VALUES
    ('Séance unitaire', 'unitaire', 'unit', 1, 30, 2500,
     'Une séance, à consommer dans les 30 jours. Idéal pour découvrir.', 10),
    ('Carnet 10 séances', 'carnet-10', 'pack', 10, 365, 18000,
     'Dix séances valables un an. 18 €/séance, l''engagement le plus apprécié.', 20),
    ('Abonnement mensuel illimité', 'abo-mensuel', 'monthly_unlimited', NULL, 31, 11000,
     'Toutes les séances que vous voulez pendant 31 jours. Renouvellement automatique.', 30)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;

-- Site settings — contenu éditable
INSERT INTO t_site_settings (setting_key, setting_value, setting_type, description) VALUES
    ('cancellation_window_hours', '24', 'int', 'Fenêtre minimale (heures) avant le cours pour annuler côté membre'),
    ('home_hero_title', 'Le silence en mouvement', 'text', 'Titre H1 de la page d''accueil'),
    ('home_hero_subtitle', 'Le yoga d''Aurane à Versailles', 'text', 'Sous-titre du hero'),
    ('signature_quote', 'Il ne faut jamais sous-estimer l''influence du hasard sur l''existence de tout être', 'text', 'Citation signature pied de page'),
    ('lieu_name', 'La Maison Amara', 'text', 'Nom du lieu où Aurane enseigne'),
    ('lieu_address', '16b Passages de la Geôle, 78000 Versailles', 'text', 'Adresse postale complète'),
    ('lieu_description', 'Un écrin lumineux au cœur de Versailles, à quelques pas du Marché Notre-Dame.', 'html', 'Description du lieu pour la page /le-lieu'),
    ('contact_email', 'aldana.aurane@hotmail.fr', 'text', 'Email de contact (réception demandes 1-1, notifs admin)'),
    ('contact_phone', '06 74 96 99 33', 'text', 'Téléphone affiché en contact'),
    ('contact_phone_intl', '+33674969933', 'text', 'Téléphone au format international (clic-to-call mobile)')
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP;
```

⚠ **Important** : le `password_hash` ci-dessus est un **placeholder non fonctionnel**. Aurane ne pourra pas se loguer tant que ce password n'est pas redéfini. Pour le redéfinir, deux options en P3 :

1. CLI script ponctuel : `php database/set_password.php aldana.aurane@hotmail.fr 'son-vrai-password'`
2. Lien de reset envoyé à son email après import production

---

## Task 15: Lancer toutes les migrations et valider la structure

**Files:** aucun nouveau, exécution + vérification.

- [ ] **Step 1: Drop la BDD et recréer vide pour tester from scratch**

```powershell
C:\wamp64\bin\mysql\mysql9.1.0\bin\mysql.exe -u root -e "DROP DATABASE IF EXISTS aldana; CREATE DATABASE aldana CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
```

- [ ] **Step 2: Lancer le runner**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php
```

Attendu : `Migrations à jouer : 13` puis 13 lignes `→ XXX_*.sql ... OK` puis `Migrations terminées avec succès.`

- [ ] **Step 3: Vérifier la liste des tables**

```powershell
C:\wamp64\bin\mysql\mysql9.1.0\bin\mysql.exe -u root aldana -e "SHOW TABLES;"
```

Attendu : 13 tables (12 métier + `t_migrations`) :
- t_bookings
- t_classes
- t_event_bookings
- t_events
- t_migrations
- t_payments
- t_practices
- t_private_requests
- t_processed_webhooks
- t_site_settings
- t_subscription_plans
- t_subscriptions
- t_users

- [ ] **Step 4: Vérifier que les seeds sont bien insérés**

```powershell
C:\wamp64\bin\mysql\mysql9.1.0\bin\mysql.exe -u root aldana -e "SELECT email, role_ID FROM t_users; SELECT slug, name FROM t_practices; SELECT slug, name, price_cents FROM t_subscription_plans; SELECT COUNT(*) AS n FROM t_site_settings;"
```

Attendu :
- 1 user : `aldana.aurane@hotmail.fr` / role 29
- 2 pratiques : `yoga` / `power-yoga`
- 3 formules : `unitaire` 2500, `carnet-10` 18000, `abo-mensuel` 11000
- 10 settings

- [ ] **Step 5: Lancer une 2e fois le runner pour valider l'idempotence**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php
```

Attendu : `Aucune migration en attente. Base à jour.` (les migrations sont tracées dans `t_migrations`, donc pas rejouées).

- [ ] **Step 6: Tester `--status`**

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php --status
```

Attendu : les 13 migrations listées avec `✓ appliquée`.

---

## Task 16: Documentation `database/README.md`

**Files:**

- Create: `database/README.md`

- [ ] **Step 1: Écrire le README**

```markdown
# Base de données ALDANA

Schéma MySQL pour le site Aurane Aldana — vitrine, booking, paiement, espace membre, admin.

## Structure

12 tables métier + 1 table de tracking des migrations.

| Table | Rôle | Domaine |
|---|---|---|
| `t_users` | Utilisateurs + rôles (29 admin, 11 prof, 10 client) | Auth |
| `t_practices` | Yoga, Power Yoga, ... | Catalogue |
| `t_subscription_plans` | Unitaire / carnets / abos | Catalogue |
| `t_events` | Week-Ends Bien-Être | Catalogue |
| `t_classes` | Cours collectifs planifiés (FK `t_practices`) | Planning |
| `t_subscriptions` | Forfaits actifs par membre (FK `t_users`, `t_subscription_plans`) | Engagement |
| `t_payments` | Historique Stripe (FK `t_users`, `t_subscriptions`, self-FK refund) | Engagement |
| `t_bookings` | Résa cours collectif (FK `t_classes`, `t_users`, `t_subscriptions`) | Booking |
| `t_event_bookings` | Résa event (FK `t_events`, `t_users`, `t_payments`) | Booking |
| `t_private_requests` | Demandes cours 1-1 (FK `t_users` nullable) | Support |
| `t_processed_webhooks` | Idempotence webhooks Stripe | Infra |
| `t_site_settings` | CMS léger clé/valeur (citation, infos lieu, ...) | Infra |
| `t_migrations` | Tracking des migrations appliquées | Infra |

## Migrations

Pattern BookConnect : fichiers numérotés `database/migrations/001_*.sql` à `013_*.sql`, exécutés dans l'ordre alphanumérique par un runner PHP.

### Lancer les migrations

```powershell
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php           # joue celles en attente
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php --status  # affiche l'état
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php --redo    # force replay de la dernière
```

Le runner trace dans `t_migrations` ce qui est déjà appliqué — idempotent par défaut. Les CREATE utilisent `IF NOT EXISTS`, les INSERT du seed utilisent `ON DUPLICATE KEY UPDATE`.

### Convention pour ajouter une migration

1. Nommer `NNN_<verbe>_<table_ou_feature>.sql` (NNN = nombre 3 chiffres incrémenté, ex: `014_add_birthdate_to_users.sql`)
2. Toujours idempotent (`IF NOT EXISTS`, `IF EXISTS`, ou helper procédure pour ALTER conditionnels)
3. Une seule responsabilité par migration
4. Tester en local : `mysql DROP/CREATE` + `migrate.php` from scratch doit aboutir à l'état attendu

## Seeds

Le fichier `013_seed_initial_data.sql` insère les données indispensables au fonctionnement :
- 1 utilisateur Aurane (role admin)
- 2 pratiques (Yoga, Power Yoga)
- 3 formules (Unitaire, Carnet 10, Abo mensuel)
- 10 settings (citation, infos lieu, fenêtre annulation, etc.)

**Tous les chiffres sont des hypothèses à valider avec Aurane** (cf spec §10).

## Conventions

- Moteur : **InnoDB** (transactions + FK)
- Charset : **utf8mb4_unicode_ci** partout
- PK : `*_ID` en `INT UNSIGNED AUTO_INCREMENT`
- FK : `ON DELETE RESTRICT` par défaut (jamais cascade silencieuse), `SET NULL` pour les soft refs
- Timestamps : `created_at` partout, `updated_at` quand pertinent
- Indexes systématiques sur FK et colonnes de filtrage temporel
- Pas de soft delete V1 (sauf users via `status='deleted'` en P3)

## Reset complet (dev only)

```powershell
C:\wamp64\bin\mysql\mysql9.1.0\bin\mysql.exe -u root -e "DROP DATABASE IF EXISTS aldana; CREATE DATABASE aldana CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
C:\wamp64\bin\php\php8.4.0\php.exe database/migrate.php
```

⚠ Jamais sur prod sans backup.
```

---

## Task 17: Commit P1

**Files:** -

- [ ] **Step 1: git status (vérification)**

```powershell
git status --short
```

Attendu : 15 fichiers nouveaux ou modifiés (1 runner + 13 migrations + 1 README).

- [ ] **Step 2: stage + commit**

```powershell
git add database/ docs/superpowers/plans/2026-05-18-aldana-p1-bdd.md
git commit -m "feat(P1): schéma BDD complet — 12 tables + migration runner idempotent

- Migration runner database/migrate.php (trace dans t_migrations)
- 12 migrations CREATE TABLE IF NOT EXISTS (001-012)
- 1 seed 013 : Aurane admin + 2 pratiques + 3 formules + 10 settings
- README database/ documentant le workflow migration et conventions
- 13 migrations jouées avec succès, idempotence vérifiée (relance no-op)
- FK déclarées sauf t_payments.event_booking_ID (cohérence applicative P5)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
```

- [ ] **Step 3: Vérifier le log**

```powershell
git log --oneline -3
```

Attendu :
```
<hash> feat(P1): schéma BDD complet — 12 tables + migration runner idempotent
d99a32b chore(P0): bascule mockup React vers squelette PHP PSR-4
122e4c4 initialisation repo
```

---

## Self-Review

**Couverture spec §4.3** : les 12 tables du spec sont créées ✓. La table `t_migrations` (tracking) est créée auto par le runner, pas dans le spec — c'est un détail d'infrastructure attendu.

**Placeholder scan** : pas de TODO/TBD dans le plan ✓. Les "hypothèses Aurane" sont explicitement marquées comme tels (§14 du plan, §10 du spec).

**Cohérence types/noms** :
- `practice_ID` cohérent entre `t_practices` (PK) et `t_classes` (FK) ✓
- `user_ID` cohérent entre `t_users` et 6 autres tables FK ✓
- `subscription_ID` cohérent entre `t_subscriptions` et `t_bookings`, `t_payments` ✓
- `payment_ID` cohérent entre `t_payments` et `t_event_bookings`, self-FK refund ✓

**FK circulaire t_payments ↔ t_event_bookings** : volontairement résolue en déclarant la FK d'un seul côté (t_event_bookings.payment_ID → t_payments.payment_ID), l'autre côté assuré par logique applicative en P5. Documenté dans le plan §10 et le seed §15.

**Ordre des migrations** : conforme au graphe de dépendances FK. Pas de FK manquante au moment du CREATE.

**Idempotence** :
- CREATE TABLE IF NOT EXISTS ✓
- INSERT ... ON DUPLICATE KEY UPDATE pour le seed ✓
- Tracking via t_migrations ✓
- Validation : `migrate.php` lancé 2 fois → 2e fois "no-op" §15 step 5

---

## Sortie attendue fin P1

- 13 migrations dans `database/migrations/` (12 schéma + 1 seed)
- Runner `database/migrate.php` fonctionnel avec 3 modes (default / --status / --redo)
- BDD `aldana` peuplée : 1 admin Aurane, 2 pratiques, 3 formules, 10 settings
- README BDD documenté
- Commit P1 sur main

**Durée totale estimée P1** : 1 demi-journée (mécanique : SQL DDL + un runner PHP simple).

**Prochain plan à écrire après P1 livrée** : `2026-05-18-aldana-p2-vitrine.md` (9 pages publiques HTML/CSS, Layout, assets, SEO basique).
