Formulaire d'infos de facturation avant paiement (#abonnement)
- Nouvelle page /compte/abonnement/mes-infos avec formulaire deux colonnes (prénom, nom, SIRET, entreprise, adresse, CP, ville, pays) - Lookup SIRET via l'annuaire des entreprises pour pré-remplissage - Cases à cocher obligatoires : renonciation rétractation + CGU (modale) - Le bouton Valider est désactivé tant que les deux cases ne sont pas cochées - Pays : liste déroulante des 20 pays de la zone euro, France par défaut - Le SIRET est stocké dans users.siret (migration 0002) pour pré-remplissage - Pré-remplissage des infos Frisbii (GET customer) si déjà client - Côté Frisbii : update_customer si client existant, create_customer sinon - Les URLs accept/cancel sont désormais transmises dans le body Frisbii - Mise à jour des CGU : distinction données decp.info vs Frisbii Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -162,6 +162,22 @@ def update_password_hash(user_id: int, password_hash: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_siret(user_id: int) -> str | None:
|
||||||
|
row = (
|
||||||
|
get_conn()
|
||||||
|
.execute("SELECT siret FROM users WHERE id = ?", (user_id,))
|
||||||
|
.fetchone()
|
||||||
|
)
|
||||||
|
return row["siret"] if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def set_siret(user_id: int, siret: str) -> None:
|
||||||
|
get_conn().execute(
|
||||||
|
"UPDATE users SET siret = ?, updated_at = ? WHERE id = ?",
|
||||||
|
(siret, _now(), user_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def set_pending_email(user_id: int, email: str) -> None:
|
def set_pending_email(user_id: int, email: str) -> None:
|
||||||
get_conn().execute(
|
get_conn().execute(
|
||||||
"UPDATE users SET pending_email = ?, updated_at = ? WHERE id = ?",
|
"UPDATE users SET pending_email = ?, updated_at = ? WHERE id = ?",
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ _MIGRATIONS: list[tuple[str, str]] = [
|
|||||||
"0001_add_prix_ht_to_subscriptions",
|
"0001_add_prix_ht_to_subscriptions",
|
||||||
"ALTER TABLE subscriptions ADD COLUMN prix_ht REAL",
|
"ALTER TABLE subscriptions ADD COLUMN prix_ht REAL",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"0002_add_siret_to_users",
|
||||||
|
"ALTER TABLE users ADD COLUMN siret TEXT",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ La période d'essai est accordée une seule fois par utilisateur.
|
|||||||
dcc.Markdown(
|
dcc.Markdown(
|
||||||
"""
|
"""
|
||||||
L'abonnement est facturé mensuellement, à la date anniversaire de la souscription.
|
L'abonnement est facturé mensuellement, à la date anniversaire de la souscription.
|
||||||
Le paiement est traité par [Frisbii](https://www.frisbii.com), prestataire de paiement en ligne.
|
Le paiement est traité par [Frisbii](https://www.frisbii.com), prestataire européen de paiement en ligne.
|
||||||
Les coordonnées bancaires sont conservées exclusivement par Frisbii et ne sont pas transmises à decp.info.
|
Les coordonnées bancaires sont conservées exclusivement par Frisbii et ne sont pas transmises à decp.info.
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
@@ -63,28 +63,26 @@ Les coordonnées bancaires sont conservées exclusivement par Frisbii et ne sont
|
|||||||
"""
|
"""
|
||||||
L'abonnement peut être résilié à tout moment depuis l'espace [Mon compte](/compte/abonnement).
|
L'abonnement peut être résilié à tout moment depuis l'espace [Mon compte](/compte/abonnement).
|
||||||
La résiliation prend effet à la fin de la période mensuelle en cours : l'accès aux fonctionnalités payantes est maintenu jusqu'à cette date, sans remboursement au prorata.
|
La résiliation prend effet à la fin de la période mensuelle en cours : l'accès aux fonctionnalités payantes est maintenu jusqu'à cette date, sans remboursement au prorata.
|
||||||
"""
|
|
||||||
),
|
|
||||||
html.H4("Droit de rétractation"),
|
|
||||||
dcc.Markdown(
|
|
||||||
"""
|
|
||||||
Conformément à l'article L221-18 du Code de la consommation, vous disposez d'un délai de 14 jours à compter de la souscription pour exercer votre droit de rétractation, sans motif à fournir.
|
|
||||||
|
|
||||||
Si vous avez demandé l'accès immédiat au service et que vous l'avez effectivement utilisé, une partie proportionnelle à l'utilisation peut être déduite du remboursement.
|
|
||||||
|
|
||||||
Pour exercer ce droit, [contactez-nous](/a-propos/contact).
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
html.H4("Données personnelles"),
|
html.H4("Données personnelles"),
|
||||||
dcc.Markdown(
|
dcc.Markdown(
|
||||||
"""
|
"""
|
||||||
La gestion de l'abonnement nécessite le traitement des données suivantes :
|
La gestion de l'abonnement implique le traitement de données personnelles, réparties entre decp.info et Frisbii (prestataire de paiement).
|
||||||
|
|
||||||
- adresse e-mail (identification du compte)
|
**Données stockées par decp.info :**
|
||||||
- historique de facturation (fourni par Frisbii)
|
|
||||||
|
- Adresse e-mail (identification du compte)
|
||||||
|
- Numéro SIRET (si renseigné, conservé pour pré-remplir les futures souscriptions)
|
||||||
|
|
||||||
|
**Données stockées par Frisbii :**
|
||||||
|
|
||||||
|
- Informations de facturation : prénom, nom, adresse postale, code postal, ville, pays, nom de l'entreprise
|
||||||
|
- Informations de paiement : coordonnées bancaires (accessibles uniquement par Frisbii, jamais transmises à decp.info)
|
||||||
|
- Historique des factures
|
||||||
|
|
||||||
Ces données sont utilisées uniquement pour la gestion de votre abonnement et ne sont pas transmises à des tiers à des fins commerciales.
|
Ces données sont utilisées uniquement pour la gestion de votre abonnement et ne sont pas transmises à des tiers à des fins commerciales.
|
||||||
Conformément au RGPD, vous pouvez demander l'accès, la rectification ou la suppression de vos données via la [page de contact](/a-propos/contact).
|
Conformément au RGPD, vous pouvez demander l'accès, la rectification ou la suppression de vos données en [me contactant](/a-propos/contact).
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
html.H4("Contact"),
|
html.H4("Contact"),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def _plan_card(meta: dict, trial: int | None, trial_used: bool):
|
|||||||
className="text-muted mb-2",
|
className="text-muted mb-2",
|
||||||
)
|
)
|
||||||
elif trial:
|
elif trial:
|
||||||
badge = html.Div(f"{trial} jours d'essai gratuit", className="mb-2")
|
badge = html.Div(f"{trial} jours d'essai gratuit", className="mb-3")
|
||||||
else:
|
else:
|
||||||
badge = None
|
badge = None
|
||||||
return dbc.Card(
|
return dbc.Card(
|
||||||
@@ -41,23 +41,18 @@ def _plan_card(meta: dict, trial: int | None, trial_used: bool):
|
|||||||
html.P(f"{meta['prix_ht']} € HT / mois", className="text-muted mb-3"),
|
html.P(f"{meta['prix_ht']} € HT / mois", className="text-muted mb-3"),
|
||||||
html.P(meta["description"], className="mb-3"),
|
html.P(meta["description"], className="mb-3"),
|
||||||
badge,
|
badge,
|
||||||
html.Form(
|
html.A(
|
||||||
method="POST",
|
"S'abonner",
|
||||||
action="/subscriptions/subscribe",
|
href=(
|
||||||
children=[
|
"#"
|
||||||
_csrf_input(),
|
if TOUS_ABONNES
|
||||||
dcc.Input(type="hidden", name="plan", value=meta["key"]),
|
else f"/compte/abonnement/mes-infos?plan={meta['key']}"
|
||||||
html.Button(
|
),
|
||||||
"S'abonner",
|
className=(
|
||||||
type="submit",
|
"btn btn-secondary disabled"
|
||||||
className=(
|
if TOUS_ABONNES
|
||||||
"btn btn-secondary disabled"
|
else "btn btn-primary"
|
||||||
if TOUS_ABONNES
|
),
|
||||||
else "btn btn-primary"
|
|
||||||
),
|
|
||||||
disabled=TOUS_ABONNES,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
className="p-4",
|
className="p-4",
|
||||||
@@ -166,7 +161,10 @@ def _active_view(row):
|
|||||||
|
|
||||||
def _feedback(query):
|
def _feedback(query):
|
||||||
msgs = {
|
msgs = {
|
||||||
"succes": ("Merci, votre abonnement est en cours d'activation.", "success"),
|
"succes": (
|
||||||
|
"Merci, votre abonnement est activé. Bonne exploration !",
|
||||||
|
"success",
|
||||||
|
),
|
||||||
"annule": ("Paiement annulé.", "secondary"),
|
"annule": ("Paiement annulé.", "secondary"),
|
||||||
}
|
}
|
||||||
out = []
|
out = []
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from dash import Input, Output, State, callback, dcc, html, register_page
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
from src.auth import db as auth_db
|
||||||
|
from src.pages._compte_shell import account_guard, account_shell
|
||||||
|
from src.subscriptions import client as frisbii_client
|
||||||
|
from src.utils.data import get_annuaire_data
|
||||||
|
|
||||||
|
register_page(
|
||||||
|
__name__,
|
||||||
|
path="/compte/abonnement/mes-infos",
|
||||||
|
title="Mes informations | Abonnement | decp.info",
|
||||||
|
name="Mes informations de facturation",
|
||||||
|
description="Informations de facturation pour votre abonnement decp.info.",
|
||||||
|
)
|
||||||
|
|
||||||
|
_CGU_MARKDOWN = """
|
||||||
|
L'accès aux fonctionnalités de base de decp.info est gratuit et sans inscription.
|
||||||
|
Un abonnement payant donne accès à des fonctionnalités supplémentaires qui nécessitent la création d'un compte.
|
||||||
|
|
||||||
|
#### Fonctionnalités incluses
|
||||||
|
|
||||||
|
Les fonctionnalités accessibles aux abonnés évoluent au fil du développement du service.
|
||||||
|
La liste à jour est visible depuis la [page d'abonnement](/compte/abonnement).
|
||||||
|
|
||||||
|
#### Tarifs
|
||||||
|
|
||||||
|
Deux formules sont proposées :
|
||||||
|
|
||||||
|
- **Abonnement** — 20 € HT / mois (soit 24 € TTC)
|
||||||
|
- **Abonnement de soutien** — 50 € HT / mois (soit 60 € TTC) — mêmes fonctionnalités, contribution renforcée au projet
|
||||||
|
|
||||||
|
La TVA applicable est de 20 %. Les prix TTC sont affichés lors de la souscription.
|
||||||
|
|
||||||
|
#### Période d'essai
|
||||||
|
|
||||||
|
Une période d'essai gratuite peut être proposée lors de la souscription.
|
||||||
|
Sa durée est indiquée avant validation. Aucun prélèvement n'est effectué pendant cette période.
|
||||||
|
À son terme, l'abonnement est activé et facturé automatiquement.
|
||||||
|
|
||||||
|
La période d'essai est accordée une seule fois par utilisateur.
|
||||||
|
|
||||||
|
#### Facturation et paiement
|
||||||
|
|
||||||
|
L'abonnement est facturé mensuellement, à la date anniversaire de la souscription.
|
||||||
|
Le paiement est traité par [Frisbii](https://www.frisbii.com), prestataire européen de paiement en ligne.
|
||||||
|
Les coordonnées bancaires sont conservées exclusivement par Frisbii et ne sont pas transmises à decp.info.
|
||||||
|
|
||||||
|
#### Résiliation
|
||||||
|
|
||||||
|
L'abonnement peut être résilié à tout moment depuis l'espace [Mon compte](/compte/abonnement).
|
||||||
|
La résiliation prend effet à la fin de la période mensuelle en cours : l'accès aux fonctionnalités payantes est maintenu jusqu'à cette date, sans remboursement au prorata.
|
||||||
|
|
||||||
|
#### Données stockées
|
||||||
|
|
||||||
|
La gestion de l'abonnement implique le traitement de données personnelles, réparties entre decp.info et Frisbii (prestataire de paiement).
|
||||||
|
|
||||||
|
**Données stockées par decp.info :**
|
||||||
|
|
||||||
|
- Adresse e-mail (identification du compte)
|
||||||
|
- Numéro SIRET (si renseigné, conservé pour pré-remplir les futures souscriptions et alimenter des fonctionnalités)
|
||||||
|
|
||||||
|
**Données stockées par Frisbii :**
|
||||||
|
|
||||||
|
- Informations de facturation : prénom, nom, adresse postale, code postal, ville, pays, nom de l'entreprise
|
||||||
|
- Informations de paiement : coordonnées bancaires (accessibles uniquement par Frisbii, jamais transmises à decp.info)
|
||||||
|
- Historique des factures
|
||||||
|
|
||||||
|
Ces données sont utilisées uniquement pour la gestion de votre abonnement et ne sont pas transmises à des tiers à des fins commerciales.
|
||||||
|
Conformément au RGPD, vous pouvez demander l'accès, la rectification ou la suppression de vos données en [me contactant](/a-propos/contact).
|
||||||
|
|
||||||
|
#### Contact
|
||||||
|
|
||||||
|
Pour toute question relative à votre abonnement : [page de contact](/a-propos/contact).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _csrf_input():
|
||||||
|
from flask_wtf.csrf import generate_csrf
|
||||||
|
|
||||||
|
return dcc.Input(type="hidden", name="csrf_token", value=generate_csrf())
|
||||||
|
|
||||||
|
|
||||||
|
def _cgu_modal():
|
||||||
|
return dbc.Modal(
|
||||||
|
[
|
||||||
|
dbc.ModalHeader(dbc.ModalTitle("Conditions générales d'utilisation")),
|
||||||
|
dbc.ModalBody(
|
||||||
|
dcc.Markdown(_CGU_MARKDOWN),
|
||||||
|
style={"maxHeight": "60vh", "overflowY": "auto"},
|
||||||
|
),
|
||||||
|
dbc.ModalFooter(
|
||||||
|
dbc.Button(
|
||||||
|
"Fermer", id="inf-cgu-close", className="ms-auto", color="secondary"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
id="inf-cgu-modal",
|
||||||
|
size="lg",
|
||||||
|
is_open=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def layout(**query):
|
||||||
|
guard = account_guard("/compte/abonnement/mes-infos", require_subscription=False)
|
||||||
|
if guard is not None:
|
||||||
|
return guard
|
||||||
|
|
||||||
|
plan = query.get("plan", "")
|
||||||
|
if not plan:
|
||||||
|
return dcc.Location(href="/compte/abonnement", id="inf-no-plan-redirect")
|
||||||
|
|
||||||
|
prefill: dict = {}
|
||||||
|
try:
|
||||||
|
prefill = frisbii_client.get_customer(f"decpinfo-{current_user.id}")
|
||||||
|
except frisbii_client.FrisbiiError:
|
||||||
|
prefill = {}
|
||||||
|
stored_siret = auth_db.get_siret(current_user.id) or ""
|
||||||
|
|
||||||
|
col1 = dbc.Col(
|
||||||
|
[
|
||||||
|
dbc.Label("Prénom *"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-prenom",
|
||||||
|
name="first_name",
|
||||||
|
type="text",
|
||||||
|
required=True,
|
||||||
|
value=prefill.get("first_name", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("Nom *"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-nom",
|
||||||
|
name="last_name",
|
||||||
|
type="text",
|
||||||
|
required=True,
|
||||||
|
value=prefill.get("last_name", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("SIRET"),
|
||||||
|
dbc.InputGroup(
|
||||||
|
[
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-siret",
|
||||||
|
name="siret",
|
||||||
|
type="text",
|
||||||
|
maxLength=14,
|
||||||
|
placeholder="14 chiffres",
|
||||||
|
value=stored_siret,
|
||||||
|
),
|
||||||
|
dbc.Button(
|
||||||
|
"Récupérer les autres infos",
|
||||||
|
id="inf-siret-btn",
|
||||||
|
color="secondary",
|
||||||
|
type="button",
|
||||||
|
n_clicks=0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="mb-1",
|
||||||
|
),
|
||||||
|
html.Div(id="inf-siret-msg", className="small mb-3"),
|
||||||
|
dbc.Label("Nom de l'entreprise"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-entreprise",
|
||||||
|
name="company",
|
||||||
|
type="text",
|
||||||
|
value=prefill.get("company", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
md=6,
|
||||||
|
)
|
||||||
|
|
||||||
|
col2 = dbc.Col(
|
||||||
|
[
|
||||||
|
dbc.Label("Adresse ligne 1 *"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-adresse1",
|
||||||
|
name="address",
|
||||||
|
type="text",
|
||||||
|
required=True,
|
||||||
|
value=prefill.get("address", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("Adresse ligne 2"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-adresse2",
|
||||||
|
name="address2",
|
||||||
|
type="text",
|
||||||
|
value=prefill.get("address2", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("Code postal *"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-cp",
|
||||||
|
name="postal_code",
|
||||||
|
type="text",
|
||||||
|
required=True,
|
||||||
|
value=prefill.get("postal_code", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("Ville *"),
|
||||||
|
dbc.Input(
|
||||||
|
id="inf-ville",
|
||||||
|
name="city",
|
||||||
|
type="text",
|
||||||
|
required=True,
|
||||||
|
value=prefill.get("city", ""),
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
dbc.Label("Pays *"),
|
||||||
|
dbc.Select(
|
||||||
|
id="inf-pays",
|
||||||
|
name="country",
|
||||||
|
value=prefill.get("country", "FR"),
|
||||||
|
options=[
|
||||||
|
{"label": "France", "value": "FR"},
|
||||||
|
{"label": "Allemagne", "value": "DE"},
|
||||||
|
{"label": "Autriche", "value": "AT"},
|
||||||
|
{"label": "Belgique", "value": "BE"},
|
||||||
|
{"label": "Chypre", "value": "CY"},
|
||||||
|
{"label": "Croatie", "value": "HR"},
|
||||||
|
{"label": "Espagne", "value": "ES"},
|
||||||
|
{"label": "Estonie", "value": "EE"},
|
||||||
|
{"label": "Finlande", "value": "FI"},
|
||||||
|
{"label": "Grèce", "value": "GR"},
|
||||||
|
{"label": "Irlande", "value": "IE"},
|
||||||
|
{"label": "Italie", "value": "IT"},
|
||||||
|
{"label": "Lettonie", "value": "LV"},
|
||||||
|
{"label": "Lituanie", "value": "LT"},
|
||||||
|
{"label": "Luxembourg", "value": "LU"},
|
||||||
|
{"label": "Malte", "value": "MT"},
|
||||||
|
{"label": "Pays-Bas", "value": "NL"},
|
||||||
|
{"label": "Portugal", "value": "PT"},
|
||||||
|
{"label": "Slovaquie", "value": "SK"},
|
||||||
|
{"label": "Slovénie", "value": "SI"},
|
||||||
|
],
|
||||||
|
className="mb-3",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
md=6,
|
||||||
|
)
|
||||||
|
|
||||||
|
checkboxes = html.Div(
|
||||||
|
[
|
||||||
|
dcc.Checklist(
|
||||||
|
id="inf-cb-retractation",
|
||||||
|
options=[
|
||||||
|
{
|
||||||
|
"label": "Je renonce à mon droit de rétractation légal de 14 jours.",
|
||||||
|
"value": "ok",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
value=[],
|
||||||
|
className="mb-2",
|
||||||
|
),
|
||||||
|
dcc.Checklist(
|
||||||
|
id="inf-cb-cgu",
|
||||||
|
options=[
|
||||||
|
{
|
||||||
|
"label": [
|
||||||
|
"J'ai lu et accepte les ",
|
||||||
|
html.A(
|
||||||
|
"conditions générales d'utilisation du service",
|
||||||
|
href="#",
|
||||||
|
id="inf-cgu-link",
|
||||||
|
style={"cursor": "pointer"},
|
||||||
|
),
|
||||||
|
".",
|
||||||
|
],
|
||||||
|
"value": "ok",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
value=[],
|
||||||
|
className="mb-4",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
form = html.Form(
|
||||||
|
method="POST",
|
||||||
|
action="/subscriptions/subscribe",
|
||||||
|
children=[
|
||||||
|
_csrf_input(),
|
||||||
|
dcc.Input(type="hidden", name="plan", value=plan),
|
||||||
|
dbc.Row([col1, col2], className="g-4 mb-4"),
|
||||||
|
checkboxes,
|
||||||
|
html.Button(
|
||||||
|
"Suivant",
|
||||||
|
id="inf-submit",
|
||||||
|
type="submit",
|
||||||
|
className="btn btn-primary",
|
||||||
|
disabled=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return account_shell(
|
||||||
|
"abonnement",
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.H2("Mes informations de facturation", className="mb-4"),
|
||||||
|
form,
|
||||||
|
_cgu_modal(),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("inf-entreprise", "value"),
|
||||||
|
Output("inf-adresse1", "value"),
|
||||||
|
Output("inf-cp", "value"),
|
||||||
|
Output("inf-ville", "value"),
|
||||||
|
Output("inf-siret-msg", "children"),
|
||||||
|
Output("inf-siret-msg", "className"),
|
||||||
|
Input("inf-siret-btn", "n_clicks"),
|
||||||
|
State("inf-siret", "value"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def _lookup_siret(_, siret):
|
||||||
|
empty = ("", "", "", "")
|
||||||
|
if not siret or not siret.strip():
|
||||||
|
return *empty, "Veuillez saisir un SIRET.", "small text-danger mb-3"
|
||||||
|
data = get_annuaire_data(siret.strip())
|
||||||
|
if data is None:
|
||||||
|
return (
|
||||||
|
*empty,
|
||||||
|
"SIRET introuvable dans l'annuaire des entreprises.",
|
||||||
|
"small text-danger mb-3",
|
||||||
|
)
|
||||||
|
etablissement = data.get("matching_etablissements")
|
||||||
|
if not etablissement:
|
||||||
|
return (
|
||||||
|
*empty,
|
||||||
|
"Aucun établissement trouvé pour ce SIRET.",
|
||||||
|
"small text-danger mb-3",
|
||||||
|
)
|
||||||
|
etab = etablissement[0]
|
||||||
|
return (
|
||||||
|
data.get("nom_raison_sociale", ""),
|
||||||
|
etab.get("adresse", ""),
|
||||||
|
etab.get("code_postal", ""),
|
||||||
|
etab.get("libelle_commune", ""),
|
||||||
|
"Informations récupérées.",
|
||||||
|
"small text-success mb-3",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("inf-submit", "disabled"),
|
||||||
|
Input("inf-cb-retractation", "value"),
|
||||||
|
Input("inf-cb-cgu", "value"),
|
||||||
|
)
|
||||||
|
def _toggle_submit(retractation, cgu):
|
||||||
|
return not (retractation and cgu)
|
||||||
|
|
||||||
|
|
||||||
|
@callback(
|
||||||
|
Output("inf-cgu-modal", "is_open"),
|
||||||
|
Input("inf-cgu-link", "n_clicks"),
|
||||||
|
Input("inf-cgu-close", "n_clicks"),
|
||||||
|
State("inf-cgu-modal", "is_open"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def _toggle_cgu(_, __, is_open):
|
||||||
|
return not is_open
|
||||||
@@ -36,32 +36,38 @@ def _call(method: str, path: str, json: dict | None = None) -> dict:
|
|||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
raise FrisbiiError(0, str(exc)) from exc
|
raise FrisbiiError(0, str(exc)) from exc
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
|
print(resp.text)
|
||||||
raise FrisbiiError(resp.status_code, resp.text)
|
raise FrisbiiError(resp.status_code, resp.text)
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_customer(handle: str, email: str) -> dict:
|
def get_customer(handle: str) -> dict:
|
||||||
try:
|
return _call("GET", f"/v1/customer/{handle}")
|
||||||
return _call("GET", f"/v1/customer/{handle}")
|
|
||||||
except FrisbiiError as exc:
|
|
||||||
if exc.status_code != 404:
|
def update_customer(handle: str, data: dict) -> dict:
|
||||||
raise
|
return _call("PUT", f"/v1/customer/{handle}", json=data)
|
||||||
return _call("POST", "/v1/customer", json={"handle": handle, "email": email})
|
|
||||||
|
|
||||||
|
|
||||||
def create_subscription_session(
|
def create_subscription_session(
|
||||||
plan_handle: str,
|
plan_handle: str,
|
||||||
customer_handle: str,
|
|
||||||
accept_url: str,
|
accept_url: str,
|
||||||
cancel_url: str,
|
cancel_url: str,
|
||||||
no_trial: bool = False,
|
no_trial: bool = False,
|
||||||
|
customer_handle: str | None = None,
|
||||||
|
create_customer: dict | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
body: dict = {
|
body: dict = {
|
||||||
"plan": plan_handle,
|
"plan": plan_handle,
|
||||||
"customer": customer_handle,
|
|
||||||
"signup_method": "link",
|
"signup_method": "link",
|
||||||
"generate_handle": True,
|
"generate_handle": True,
|
||||||
|
"accept_url": accept_url,
|
||||||
|
"cancel_url": cancel_url,
|
||||||
}
|
}
|
||||||
|
if customer_handle:
|
||||||
|
body["customer"] = customer_handle
|
||||||
|
elif create_customer:
|
||||||
|
body["create_customer"] = create_customer
|
||||||
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)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
from flask import Blueprint, redirect, request
|
from flask import Blueprint, redirect, request
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
|
from src.auth import db as auth_db
|
||||||
from src.subscriptions import client, db, plans, webhooks
|
from src.subscriptions import client, db, plans, webhooks
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
@@ -28,19 +29,55 @@ def subscribe():
|
|||||||
cust = _customer_handle(current_user.id)
|
cust = _customer_handle(current_user.id)
|
||||||
try:
|
try:
|
||||||
meta = plans.plan_meta(plan_key)
|
meta = plans.plan_meta(plan_key)
|
||||||
client.get_or_create_customer(cust, current_user.email)
|
|
||||||
db.create_pending(
|
db.create_pending(
|
||||||
current_user.id, cust, plan_key, meta["prix_ht"] if meta else None
|
current_user.id, cust, plan_key, meta["prix_ht"] if meta else None
|
||||||
)
|
)
|
||||||
# Anti-abus : pas de nouvel essai si l'utilisateur en a déjà consommé un.
|
|
||||||
no_trial = db.has_used_trial(current_user.id)
|
no_trial = db.has_used_trial(current_user.id)
|
||||||
url = client.create_subscription_session(
|
siret = (request.form.get("siret") or "").strip()
|
||||||
handle,
|
billing: dict = {
|
||||||
cust,
|
"email": current_user.email,
|
||||||
f"{base}/compte/abonnement?paiement=succes",
|
"first_name": request.form.get("first_name", ""),
|
||||||
f"{base}/compte/abonnement?paiement=annule",
|
"last_name": request.form.get("last_name", ""),
|
||||||
no_trial=no_trial,
|
"address": request.form.get("address", ""),
|
||||||
)
|
"city": request.form.get("city", ""),
|
||||||
|
"postal_code": request.form.get("postal_code", ""),
|
||||||
|
"country": request.form.get("country", "FR"),
|
||||||
|
}
|
||||||
|
if request.form.get("address2"):
|
||||||
|
billing["address2"] = request.form["address2"]
|
||||||
|
if request.form.get("company"):
|
||||||
|
billing["company"] = request.form["company"]
|
||||||
|
|
||||||
|
if siret:
|
||||||
|
auth_db.set_siret(current_user.id, siret)
|
||||||
|
|
||||||
|
customer_exists = True
|
||||||
|
try:
|
||||||
|
client.update_customer(cust, billing)
|
||||||
|
except client.FrisbiiError as exc:
|
||||||
|
if exc.status_code != 404:
|
||||||
|
raise
|
||||||
|
customer_exists = False
|
||||||
|
|
||||||
|
if customer_exists:
|
||||||
|
url = client.create_subscription_session(
|
||||||
|
handle,
|
||||||
|
f"{base}/compte/abonnement?paiement=succes",
|
||||||
|
f"{base}/compte/abonnement?paiement=annule",
|
||||||
|
customer_handle=cust,
|
||||||
|
no_trial=no_trial,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
create_customer = {"handle": cust, **billing}
|
||||||
|
if siret:
|
||||||
|
create_customer["metadata"] = {"siret": siret}
|
||||||
|
url = client.create_subscription_session(
|
||||||
|
handle,
|
||||||
|
f"{base}/compte/abonnement?paiement=succes",
|
||||||
|
f"{base}/compte/abonnement?paiement=annule",
|
||||||
|
create_customer=create_customer,
|
||||||
|
no_trial=no_trial,
|
||||||
|
)
|
||||||
except client.FrisbiiError:
|
except client.FrisbiiError:
|
||||||
logger.exception("Échec de création de session d'abonnement Frisbii")
|
logger.exception("Échec de création de session d'abonnement Frisbii")
|
||||||
return redirect("/compte/abonnement?error=frisbii")
|
return redirect("/compte/abonnement?error=frisbii")
|
||||||
|
|||||||
Reference in New Issue
Block a user