From 1bd15f627fb6f219a3078db09b777b15fc90a1a0 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 25 Jun 2026 22:15:31 +0200 Subject: [PATCH] =?UTF-8?q?Mise=20un=20place=20d'un=20syst=C3=A8me=20DIY?= =?UTF-8?q?=20de=20migrations=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 18 +++++++++++++++ src/migrations.py | 45 +++++++++++++++++++++++++++++++++++++ src/subscriptions/db.py | 14 +++++++----- src/subscriptions/routes.py | 5 ++++- src/subscriptions/setup.py | 2 ++ 5 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/migrations.py diff --git a/CLAUDE.md b/CLAUDE.md index f4cbc8e..a4a7eda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,24 @@ Tests require a running Chrome/Chromium browser. They use `DashComposite` from ` - `DEVELOPMENT=true` enables debug logging and is set automatically during tests - `.env` file is required at runtime (copy from `template.env`) +### Migrations de schéma SQLite + +Les migrations sont gérées dans `src/migrations.py` via une liste `_MIGRATIONS` de tuples `(id, sql)`. Elles sont appliquées automatiquement au démarrage de l'app (via `init_subscriptions`). + +Pour ajouter une migration : + +```python +# src/migrations.py +_MIGRATIONS = [ + ("0001_add_prix_ht_to_subscriptions", "ALTER TABLE subscriptions ADD COLUMN prix_ht REAL"), + ("0002_ma_nouvelle_migration", "ALTER TABLE ... "), # ajouter ici +] +``` + +- L'ID doit être unique et croissant (convention `NNNN_description`) +- Les migrations appliquées sont tracées dans la table `schema_migrations` +- `apply_pending()` est idempotent : sans effet si la migration est déjà enregistrée, et tolère le cas où la colonne existe déjà dans le schéma (DB fraîche) + ### Deployment - `main` branch → manual deploy to decp.info via GitHub Actions diff --git a/src/migrations.py b/src/migrations.py new file mode 100644 index 0000000..e023bf5 --- /dev/null +++ b/src/migrations.py @@ -0,0 +1,45 @@ +""" +Migrations de schéma SQLite. + +Ajouter une migration : append un tuple (id, sql) à _MIGRATIONS. +L'id doit être unique et croissant (convention : NNNN_description). +apply_pending() est idempotent ; elle peut être appelée à chaque démarrage. +""" + +import sqlite3 +from datetime import datetime, timezone + +from src.auth.db import get_conn + +_MIGRATIONS: list[tuple[str, str]] = [ + ( + "0001_add_prix_ht_to_subscriptions", + "ALTER TABLE subscriptions ADD COLUMN prix_ht REAL", + ), +] + + +def apply_pending() -> None: + conn = get_conn() + conn.execute( + "CREATE TABLE IF NOT EXISTS schema_migrations " + "(id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)" + ) + applied = { + row[0] for row in conn.execute("SELECT id FROM schema_migrations").fetchall() + } + now = datetime.now(timezone.utc).isoformat() + for migration_id, sql in _MIGRATIONS: + if migration_id not in applied: + try: + conn.execute(sql) + except sqlite3.OperationalError as exc: + # SQLite ne supporte pas ALTER TABLE … ADD COLUMN IF NOT EXISTS. + # Sur une DB fraîche (schéma déjà à jour), on ignore l'erreur. + if "duplicate column name" not in str(exc): + raise + conn.execute( + "INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)", + (migration_id, now), + ) + conn.commit() diff --git a/src/subscriptions/db.py b/src/subscriptions/db.py index 9935eba..b61458f 100644 --- a/src/subscriptions/db.py +++ b/src/subscriptions/db.py @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS subscriptions ( frisbii_customer_handle TEXT, frisbii_subscription_handle TEXT, plan TEXT, + prix_ht REAL, status TEXT, current_period_end TEXT, trial_used INTEGER NOT NULL DEFAULT 0, @@ -31,16 +32,19 @@ def init_schema() -> None: get_conn().executescript(SUBSCRIPTIONS_SCHEMA) -def create_pending(user_id: int, customer_handle: str, plan: str) -> None: +def create_pending( + user_id: int, customer_handle: str, plan: str, prix_ht: float | None = None +) -> None: now = _now() get_conn().execute( "INSERT INTO subscriptions " - "(user_id, frisbii_customer_handle, plan, status, created_at, updated_at) " - "VALUES (?, ?, ?, 'pending', ?, ?) " + "(user_id, frisbii_customer_handle, plan, prix_ht, status, created_at, updated_at) " + "VALUES (?, ?, ?, ?, 'pending', ?, ?) " "ON CONFLICT(user_id) DO UPDATE SET " "frisbii_customer_handle=excluded.frisbii_customer_handle, " - "plan=excluded.plan, status='pending', updated_at=excluded.updated_at", - (user_id, customer_handle, plan, now, now), + "plan=excluded.plan, prix_ht=excluded.prix_ht, " + "status='pending', updated_at=excluded.updated_at", + (user_id, customer_handle, plan, prix_ht, now, now), ) diff --git a/src/subscriptions/routes.py b/src/subscriptions/routes.py index 3ba142e..b63bd8e 100644 --- a/src/subscriptions/routes.py +++ b/src/subscriptions/routes.py @@ -27,8 +27,11 @@ def subscribe(): cust = _customer_handle(current_user.id) try: + meta = plans.plan_meta(plan_key) client.get_or_create_customer(cust, current_user.email) - db.create_pending(current_user.id, cust, plan_key) + db.create_pending( + current_user.id, cust, plan_key, meta["prix_ht"] if meta else None + ) # Anti-abus : pas de nouvel essai si l'utilisateur en a déjà consommé un. no_trial = db.has_used_trial(current_user.id) url = client.create_subscription_session( diff --git a/src/subscriptions/setup.py b/src/subscriptions/setup.py index 2f77e22..3cf585e 100644 --- a/src/subscriptions/setup.py +++ b/src/subscriptions/setup.py @@ -2,6 +2,7 @@ import os from flask import Flask +from src import migrations from src.subscriptions import db from src.utils import logger @@ -15,6 +16,7 @@ _REQUIRED_ENV = ( def init_subscriptions(app: Flask) -> None: db.init_schema() + migrations.apply_pending() from src.subscriptions.routes import subscriptions_bp, webhook