feat(abonnement): routes subscribe/cancel/webhook + câblage app (#90)

This commit is contained in:
Colin Maudry
2026-06-25 19:00:53 +02:00
parent 4b1d8bf5c3
commit 3d1cb69463
6 changed files with 283 additions and 0 deletions
+4
View File
@@ -90,6 +90,10 @@ from src.api import init_api # noqa: E402 # inline: src.db.conn must be ready
init_api(app.server)
from src.subscriptions.setup import init_subscriptions # noqa: E402
init_subscriptions(app.server)
# robots.txt
@app.server.route("/robots.txt")
+78
View File
@@ -0,0 +1,78 @@
import os
from flask import Blueprint, redirect, request
from flask_login import current_user, login_required
from src.subscriptions import client, db, plans, webhooks
from src.utils import logger
subscriptions_bp = Blueprint("subscriptions", __name__)
def _customer_handle(user_id: int) -> str:
return f"decpinfo-{user_id}"
@subscriptions_bp.route("/subscriptions/subscribe", methods=["POST"])
@login_required
def subscribe():
plan_key = request.form.get("plan") or ""
handle = plans.resolve_handle(plan_key)
if handle is None:
return "Plan inconnu", 400
base = os.getenv("APP_BASE_URL", "")
cust = _customer_handle(current_user.id)
try:
client.get_or_create_customer(cust, current_user.email)
db.create_pending(current_user.id, cust, plan_key)
# 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(
handle,
cust,
f"{base}/compte/abonnement?paiement=succes",
f"{base}/compte/abonnement?paiement=annule",
no_trial=no_trial,
)
except client.FrisbiiError:
logger.exception("Échec de création de session d'abonnement Frisbii")
return redirect("/compte/abonnement?error=frisbii")
return redirect(url, code=303)
@subscriptions_bp.route("/subscriptions/cancel", methods=["POST"])
@login_required
def cancel():
row = db.get_by_user(current_user.id)
if row is None or not row["frisbii_subscription_handle"]:
return "Aucun abonnement à résilier", 400
try:
sub = client.cancel_subscription(row["frisbii_subscription_handle"])
except client.FrisbiiError:
logger.exception("Échec de résiliation Frisbii")
return redirect("/compte/abonnement?error=frisbii")
db.set_cancelled(current_user.id, sub.get("expires"))
return redirect("/compte/abonnement?resiliation=ok")
@subscriptions_bp.route("/frisbii/webhook", methods=["POST"])
def webhook():
payload = request.get_json(silent=True) or {}
if not webhooks.verify_signature(payload, os.getenv("FRISBII_WEBHOOK_SECRET", "")):
return "", 403
customer = payload.get("customer")
sub_handle = payload.get("subscription")
if not customer or db.get_by_customer(customer) is None:
return "", 200 # rien à rapprocher
try:
sub = client.get_subscription(sub_handle) if sub_handle else None
except client.FrisbiiError:
logger.exception("Webhook : lecture de l'abonnement Frisbii impossible")
return "", 502 # Frisbii réessaiera
if sub is None:
return "", 200
status, current_period_end = webhooks.map_subscription(sub)
db.update_from_webhook(customer, sub_handle, status, current_period_end)
return "", 200
+35
View File
@@ -0,0 +1,35 @@
import os
from flask import Flask
from src.subscriptions import db
from src.utils import logger
_REQUIRED_ENV = (
"FRISBII_API_KEY",
"FRISBII_WEBHOOK_SECRET",
"FRISBII_PLAN_SIMPLE",
"FRISBII_PLAN_SOUTIEN",
)
def init_subscriptions(app: Flask) -> None:
db.init_schema()
from src.subscriptions.routes import subscriptions_bp, webhook
app.register_blueprint(subscriptions_bp)
# Le webhook reçoit des POST externes : pas de jeton CSRF possible.
from src.auth.setup import _csrf as _auth_csrf
if _auth_csrf is not None:
_auth_csrf.exempt(webhook) # exempte la seule vue webhook
missing = [name for name in _REQUIRED_ENV if not os.getenv(name)]
if missing:
logger.warning(
"Variables Frisbii manquantes (%s) : les abonnements échoueront. "
"Définissez-les dans .env (voir .template.env).",
", ".join(missing),
)