docs(abonnement): design changement de méthode de paiement (#108)
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user