Fix redirection Frisbii après ajout de moyen de paiement au premier abonnement

accept_url/cancel_url n'existent pas dans le schéma de POST /v1/subscription
(ils y sont silencieusement ignorés) : Frisbii attend ces paramètres en
query string sur le lien hosted_page_links.payment_info retourné. Ajout
d'un warning loggé sur signature de webhook invalide, jusque-là silencieuse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-07-02 17:56:19 +02:00
parent ea20491e9c
commit 1159697c0e
3 changed files with 25 additions and 4 deletions
+13 -3
View File
@@ -1,4 +1,5 @@
import os import os
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
import httpx import httpx
@@ -61,6 +62,13 @@ def update_customer(handle: str, data: dict) -> dict:
return _call("PUT", f"/v1/customer/{handle}", json=data) return _call("PUT", f"/v1/customer/{handle}", json=data)
def _with_query(url: str, **params: str) -> str:
parts = urlsplit(url)
query = dict(parse_qsl(parts.query))
query.update(params)
return urlunsplit(parts._replace(query=urlencode(query)))
def create_subscription_session( def create_subscription_session(
plan_handle: str, plan_handle: str,
handle: str, handle: str,
@@ -70,12 +78,13 @@ def create_subscription_session(
customer_handle: str | None = None, customer_handle: str | None = None,
create_customer: dict | None = None, create_customer: dict | None = None,
) -> str: ) -> str:
# CreateSubscription (POST /v1/subscription) n'a pas de champs accept_url/
# cancel_url : ils doivent être ajoutés en query string sur le lien
# hosted_page_links.payment_info renvoyé, pas dans le body (cf. doc Frisbii).
body: dict = { body: dict = {
"plan": plan_handle, "plan": plan_handle,
"signup_method": "link", "signup_method": "link",
"handle": handle, "handle": handle,
"accept_url": accept_url,
"cancel_url": cancel_url,
} }
if customer_handle: if customer_handle:
body["customer"] = customer_handle body["customer"] = customer_handle
@@ -84,7 +93,8 @@ def create_subscription_session(
if no_trial: if no_trial:
body["no_trial"] = True body["no_trial"] = True
data = _call("POST", "/v1/subscription", json=body) data = _call("POST", "/v1/subscription", json=body)
return data["hosted_page_links"]["payment_info"] payment_info = data["hosted_page_links"]["payment_info"]
return _with_query(payment_info, accept_url=accept_url, cancel_url=cancel_url)
def create_recurring_session( def create_recurring_session(
+3
View File
@@ -148,6 +148,9 @@ def cancel():
def webhook(): def webhook():
payload = request.get_json(silent=True) or {} payload = request.get_json(silent=True) or {}
if not webhooks.verify_signature(payload, os.getenv("FRISBII_WEBHOOK_SECRET", "")): if not webhooks.verify_signature(payload, os.getenv("FRISBII_WEBHOOK_SECRET", "")):
logger.warning(
"Webhook Frisbii : signature invalide (event %s)", payload.get("id")
)
return "", 403 return "", 403
customer = payload.get("customer") customer = payload.get("customer")
+9 -1
View File
@@ -43,7 +43,11 @@ def test_create_subscription_session_with_customer_handle(fake_httpx):
"https://app/ko", "https://app/ko",
customer_handle="colibre-1", customer_handle="colibre-1",
) )
assert url == "https://checkout.reepay.com/#/sub-1" assert url == (
"https://checkout.reepay.com/"
"?accept_url=https%3A%2F%2Fapp%2Fok&cancel_url=https%3A%2F%2Fapp%2Fko"
"#/sub-1"
)
body = fake_httpx["calls"][0]["json"] body = fake_httpx["calls"][0]["json"]
assert body["plan"] == "plan_simple" assert body["plan"] == "plan_simple"
assert body["handle"] == "abo-1-1" assert body["handle"] == "abo-1-1"
@@ -51,6 +55,10 @@ def test_create_subscription_session_with_customer_handle(fake_httpx):
assert body["signup_method"] == "link" assert body["signup_method"] == "link"
assert "generate_handle" not in body assert "generate_handle" not in body
assert "prepare_subscription" not in body assert "prepare_subscription" not in body
# accept_url/cancel_url ne sont pas des champs de CreateSubscription :
# Frisbii les ignore silencieusement s'ils sont dans le body.
assert "accept_url" not in body
assert "cancel_url" not in body
def test_create_subscription_session_no_trial(fake_httpx): def test_create_subscription_session_no_trial(fake_httpx):