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

125 lines
5.0 KiB
Markdown

# 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 :
```python
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()` :
```python
@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).