Files
colibre/docs/superpowers/specs/2026-07-05-changer-methode-paiement-design.md

5.0 KiB

Changer de méthode de paiement (issue #108) — Design

Date : 2026-07-05 Branche : dev

Objectif

Permettre à un·e abonné·e actif·ve ou en essai de changer sa carte bancaire depuis /compte/abonnement, sans repasser par tout le flux d'inscription.

Mécanisme Frisbii

GET /v1/subscription/{handle} (déjà implémenté : client.get_subscription) renvoie un champ hosted_page_links.payment_info : une page hébergée par Frisbii, dédiée au changement de carte sur un abonnement existant (doc : https://docs.frisbii.com/docs/change-payment-method-on-existing-subscription). Elle accepte accept_url et cancel_url en query params pour rediriger après succès/annulation.

Ce mécanisme est plus simple que le flux add-payment existant (Checkout API /v1/session/recurring + callback qui associe la nouvelle méthode via set_subscription_payment_method) : pas de session à créer, pas de callback à gérer côté colibre, Frisbii associe directement la nouvelle carte à l'abonnement.

Portée

Bouton « Changer de méthode de paiement » affiché uniquement pour row["status"] in ("trial", "active") :

  • pending garde son bouton actuel « Ajouter une méthode de paiement » (aucune carte n'existe encore, flux différent).
  • cancelled n'a pas ce bouton : l'abonnement s'arrête à la fin de la période en cours, il n'y a plus rien à facturer dessus. Le chemin logique est de reprendre un abonnement (_reabo_button, déjà géré ailleurs).

Fichiers touchés

src/subscriptions/client.py

Nouvelle fonction :

def get_payment_info_url(sub_handle: str, accept_url: str, cancel_url: str) -> str:
    sub = get_subscription(sub_handle)
    url = sub["hosted_page_links"]["payment_info"]
    parts = urlsplit(url)
    query = dict(parse_qsl(parts.query))
    query["accept_url"] = accept_url
    query["cancel_url"] = cancel_url
    return urlunsplit(parts._replace(query=urlencode(query)))

Utilise urllib.parse pour fusionner proprement avec une éventuelle query string déjà présente sur payment_info plutôt que de la concaténer naïvement.

src/subscriptions/routes.py

Nouvelle route, symétrique à add_payment() / cancel() :

@subscriptions_bp.route("/subscriptions/change-payment-method", methods=["POST"])
@login_required
def change_payment_method():
    base = os.getenv("APP_BASE_URL", "")
    row = db.get_current(current_user.id)
    if row is None or not row["frisbii_subscription_handle"]:
        return "Aucun abonnement actif", 400
    try:
        url = client.get_payment_info_url(
            row["frisbii_subscription_handle"],
            f"{base}/compte/abonnement?carte=succes",
            f"{base}/compte/abonnement?carte=annule",
        )
    except client.FrisbiiError:
        logger.exception("Échec de récupération du lien de paiement Frisbii")
        return redirect("/compte/abonnement?error=frisbii")
    return redirect(url, code=303)

Pas de webhook/callback à gérer : Frisbii associe la nouvelle méthode de paiement à l'abonnement de son côté, et le webhook existant (/frisbii/webhook) continuera de refléter l'état de l'abonnement comme aujourd'hui.

src/pages/compte/abonnement.py

  • _active_view(row) : pour row["status"] in ("trial", "active"), ajouter un html.Form POST vers /subscriptions/change-payment-method (CSRF token via _csrf_input()), bouton btn btn-outline-secondary « Changer de méthode de paiement », affiché à côté du bouton « Me désabonner » existant.
  • _feedback(query) : ajouter la gestion de query.get("carte") :
    • "succes" → alerte success « Méthode de paiement mise à jour. »
    • "annule" → alerte secondary « Modification annulée. »

Gestion d'erreurs

  • Pas d'abonnement / pas de handle → 400 (cas normalement inatteignable depuis l'UI, le bouton n'étant rendu que si row existe et a un statut trial/active, donc un handle).
  • Échec API Frisbii (FrisbiiError) → logger.exception + redirect("/compte/abonnement?error=frisbii"), réutilise l'alerte « Une erreur est survenue avec le service de paiement. » déjà gérée par _feedback.

Tests

  • tests/subscriptions/ : test de client.get_payment_info_url — mock de client.get_subscription (ou de _call), vérifie que accept_url et cancel_url sont bien ajoutés à l'URL, y compris si payment_info a déjà une query string.
  • Test de la route change_payment_method : redirect 303 vers l'URL Frisbii quand un abonnement actif existe ; 400 si pas d'abonnement.
  • Test de _feedback() pour les nouvelles clés carte=succes / carte=annule.

Hors périmètre (YAGNI)

  • Affichage de la carte actuellement enregistrée (marque, 4 derniers chiffres) — pourra venir plus tard via client.get_customer_payment_methods.
  • Bouton pour cancelled (cf. Portée ci-dessus).
  • Gestion multi-méthodes de paiement (le champ active_payment_methods de Frisbii ne contient au plus qu'un élément dans notre usage actuel).