Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
28 KiB
Refonte du tunnel d'abonnement — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Rendre l'offre d'abonnement visible aux visiteurs non connectés et raccourcir le tunnel d'inscription/abonnement.
Architecture: Les composants « cards » d'abonnement quittent la page authentifiée /compte/abonnement pour la page publique /a-propos/abonnement, complétés d'un unique bouton « Je m'abonne » dont la cible dépend de l'état de connexion. Le choix du plan (simple/soutien) migre dans le formulaire mes-infos. La validation d'email connecte automatiquement l'utilisateur et le dépose dans mes-infos.
Tech Stack: Dash 3.4, Dash Bootstrap Components, Flask + Flask-Login, SQLite (users.sqlite), pytest.
Global Constraints
- Interface et libellés en français.
- Imports du code applicatif toujours préfixés
src.(ex.src.pages.compte.abonnement). - Lancer les tests avec
uv run pytest(l'activation de venv via Bash n'est pas fiable ici). - Soumission de formulaire native : seul un
dcc.Input/dbc.Inputportant un attributnameest soumis dans le POST d'unhtml.Form. Une interaction Dash contrôlée (clic sur carte,RadioItems…) n'est pas soumise nativement — la relier à undcc.Input(type="hidden", name=...)via callback.html.Inputn'existe pas en Dash 3.4. - Boutons : classes Bootstrap existantes (
btn btn-primary,btn btn-outline-danger…), cohérentes avec le reste du site. - Chaque commit doit passer
uv run pytest(au minimum les tests touchés).
Task 1 : linkedin_button paramétrable + inscription vers le tunnel
Le bouton LinkedIn est réutilisé par /connexion et /inscription. On lui ajoute un
paramètre next_url optionnel pour que l'inscription via LinkedIn finisse dans
mes-infos (au lieu du fallback /compte/admin).
Files:
- Modify:
src/pages/connexion.py(fonctionlinkedin_button, lignes 31-37) - Modify:
src/pages/inscription.py(appellinkedin_button(), ligne 73) - Test:
tests/test_linkedin_button.py(créer)
Interfaces:
-
Produces:
linkedin_button(next_url: str | None = None) -> html.A -
Step 1 : Écrire le test qui échoue
# tests/test_linkedin_button.py
def test_linkedin_button_default_has_no_next():
from src.pages.connexion import linkedin_button
html_str = str(linkedin_button())
assert "href='/auth/linkedin'" in html_str
assert "next=" not in html_str
def test_linkedin_button_with_next_appends_query():
from src.pages.connexion import linkedin_button
html_str = str(linkedin_button("/compte/abonnement/mes-infos"))
assert "/auth/linkedin?next=/compte/abonnement/mes-infos" in html_str
def test_inscription_linkedin_targets_mes_infos():
from src.pages import inscription
assert "/auth/linkedin?next=/compte/abonnement/mes-infos" in str(
inscription.layout()
)
- Step 2 : Lancer le test pour vérifier l'échec
Run: uv run pytest tests/test_linkedin_button.py -v
Expected: FAIL (linkedin_button() prend 0 argument / le next n'est pas présent)
- Step 3 : Implémenter
Dans src/pages/connexion.py, remplacer la fonction linkedin_button :
def linkedin_button(next_url: str | None = None):
href = "/auth/linkedin"
if next_url:
href += f"?next={next_url}"
return html.A(
"Connexion avec LinkedIn",
href=href,
className="btn w-100 mb-2",
style={"backgroundColor": "rgb(10, 102, 194)", "color": "white"},
)
Dans src/pages/inscription.py, ligne 73, remplacer linkedin_button(), par :
linkedin_button("/compte/abonnement/mes-infos"),
- Step 4 : Lancer le test pour vérifier le succès
Run: uv run pytest tests/test_linkedin_button.py -v
Expected: PASS (3 tests)
- Step 5 : Commit
git add src/pages/connexion.py src/pages/inscription.py tests/test_linkedin_button.py
git commit -m "feat(abonnement): linkedin_button paramétrable par next, inscription vers mes-infos"
Task 2 : Auto-login à la validation d'email
verify_email doit connecter automatiquement l'utilisateur et le rediriger vers
mes-infos au lieu de /connexion?verified=1.
Files:
- Modify:
src/auth/routes.py(fonctionverify_email, lignes 92-101) - Test:
tests/auth/test_verify_email.py(modifier le test du token valide)
Interfaces:
-
Consumes:
db.get_user_by_id,db.set_email_verified,tokens.consume_verification_token,User,login_user(déjà importés dans le module). -
Step 1 : Adapter le test pour qu'il échoue
Dans tests/auth/test_verify_email.py, remplacer test_verify_email_valid_token_marks_user_verified par :
def test_verify_email_valid_token_logs_in_and_redirects_to_mes_infos(
client, users_db_path
):
db.init_schema()
uid = db.create_user("a@b.c", "hash")
token = tokens.create_verification_token(uid)
resp = client.get(f"/auth/verify-email?token={token}")
assert resp.status_code == 302
assert "/compte/abonnement/mes-infos" in resp.headers["Location"]
assert db.get_user_by_id(uid)["email_verified"] == 1
with client.session_transaction() as sess:
assert sess.get("_user_id") == str(uid)
- Step 2 : Lancer le test pour vérifier l'échec
Run: uv run pytest tests/auth/test_verify_email.py -v
Expected: FAIL (Location vaut encore /connexion?verified=1, session vide)
- Step 3 : Implémenter
Dans src/auth/routes.py, dans verify_email, remplacer les deux dernières lignes
(db.set_email_verified(user_id) + return redirect("/connexion?verified=1")) par :
db.set_email_verified(user_id)
login_user(User(db.get_user_by_id(user_id)), remember=True)
return redirect("/compte/abonnement/mes-infos")
- Step 4 : Lancer les tests pour vérifier le succès
Run: uv run pytest tests/auth/test_verify_email.py -v
Expected: PASS (4 tests — les 3 autres restent inchangés)
- Step 5 : Commit
git add src/auth/routes.py tests/auth/test_verify_email.py
git commit -m "feat(auth): auto-login à la validation d'email, redirection vers mes-infos"
Task 3 : Page publique /a-propos/abonnement (cards + bouton « Je m'abonne »)
On installe dans la page publique : les cards informatives (sans bouton par card), l'explainer, et le bouton unique conditionnel. On retire la sous-section « Fonctionnalités incluses » des CGU (doublon avec l'explainer).
Files:
- Modify:
src/pages/a_propos/abonnement.py - Test:
tests/test_abonnement_public.py(créer)
Interfaces:
-
Produces:
_plan_card(meta: dict, trial: int | None) -> dbc.Card_plan_cards(trial_for=plans.trial_days) -> dbc.Row_explainer() -> dbc.Row_subscribe_button(authenticated: bool, has_active_subscription: bool, tous_abonnes: bool) -> html.Divabonnement_featuresetsubscription_termsrestent exportés (déjà consommés parcompte/abonnement_mes_infos.py).
-
Step 1 : Écrire les tests qui échouent
# tests/test_abonnement_public.py
import pytest
@pytest.fixture(autouse=True)
def _plan_env(monkeypatch):
monkeypatch.setenv("FRISBII_PLAN_SIMPLE", "plan_simple")
monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien")
def test_plan_cards_informative_no_button():
from src.pages.a_propos import abonnement as page
text = str(page._plan_cards(trial_for=lambda key: 2))
assert "Abonnement" in text
assert "Abonnement de soutien" in text
assert "2 jours d'essai gratuit" in text
assert "S'abonner" not in text
def test_subscribe_button_visitor_goes_to_inscription():
from src.pages.a_propos import abonnement as page
text = str(page._subscribe_button(False, False, False))
assert "Je m'abonne" in text
assert "href='/inscription'" in text
def test_subscribe_button_authenticated_no_sub_goes_to_mes_infos():
from src.pages.a_propos import abonnement as page
text = str(page._subscribe_button(True, False, False))
assert "href='/compte/abonnement/mes-infos'" in text
def test_subscribe_button_active_sub_manages():
from src.pages.a_propos import abonnement as page
text = str(page._subscribe_button(True, True, False))
assert "Gérer mon abonnement" in text
assert "href='/compte/abonnement'" in text
def test_subscribe_button_disabled_when_tous_abonnes():
from src.pages.a_propos import abonnement as page
text = str(page._subscribe_button(False, False, True))
assert "disabled" in text
def test_cgu_terms_trimmed_of_features_section():
from src.pages.a_propos import abonnement as page
text = str(page.subscription_terms)
assert "Fonctionnalités incluses" not in text
assert "Résiliation" in text
assert "Tarifs" in text
- Step 2 : Lancer les tests pour vérifier l'échec
Run: uv run pytest tests/test_abonnement_public.py -v
Expected: FAIL (_plan_cards/_subscribe_button inexistants ; « Fonctionnalités incluses » encore présent)
- Step 3 : Implémenter
Dans src/pages/a_propos/abonnement.py :
3a. Ajouter les imports en tête :
import dash_bootstrap_components as dbc
from dash import dcc, html, register_page
from flask_login import current_user
from src.pages._apropos_shell import apropos_shell
from src.subscriptions import db as sub_db
from src.subscriptions import plans
from src.utils import TOUS_ABONNES
from src.utils.seo import META_CONTENT
3b. Ajouter les composants cards (avant subscription_terms) :
def _plan_card(meta: dict, trial: int | None):
badge = (
html.Div(f"{trial} jours d'essai gratuit", className="mb-3") if trial else None
)
return dbc.Card(
dbc.CardBody(
[
html.H4(meta["label"], className="mb-1"),
html.P(
f"{meta['prix_ht']} € HT / mois "
f"({str(int(meta['prix_ht']) * 1.2).replace('.0', '')} € TTC)",
className="text-muted mb-3",
),
html.P(meta["description"], className="mb-3"),
badge,
],
className="p-4",
),
className="h-100",
)
def _plan_cards(trial_for=plans.trial_days):
cards = []
for key in ("simple", "soutien"):
meta = plans.plan_meta(key)
if meta:
cards.append(_plan_card(meta, trial_for(key)))
return dbc.Row([dbc.Col(c, md=6) for c in cards], className="g-4 mb-4")
def _explainer():
col_left = dbc.Col(
[html.H4("Fonctionnalités réservées aux abonné·es :"), abonnement_features],
md=6,
style={
"borderRight": "1px solid var(--bs-border-color)",
"paddingRight": "2rem",
},
)
col_right = dbc.Col(
[
html.H4("Ce que les abonnements permettent"),
html.Ul(
[
html.Li(
"passer plus de temps à développer colibre et moins de temps "
"à chercher des missions"
),
html.Li(
"rédaction d'études à partir des données, par exemple sur les "
"acheteurs dont les données sont introuvables et les raisons "
"de cette non-publication."
),
html.Li(
"fédération des bonnes volontés souhaitant militer pour une "
"législation plus ambitieuse sur la transparence de la "
"commande publique."
),
]
),
],
md=6,
style={"paddingLeft": "2rem"},
)
return dbc.Row([col_left, col_right], className="align-items-start pt-2 mb-4")
def _subscribe_button(
authenticated: bool, has_active_subscription: bool, tous_abonnes: bool
):
if tous_abonnes:
return html.Div(
[
dbc.Alert(
"Les fonctionnalités normalement accessibles contre un abonnement "
"de 20 € HT par mois sont accessibles à tous et toutes en attendant "
"la validation de mon dossier pour recevoir des paiements.",
color="info",
),
html.A(
"Je m'abonne",
href="#",
className="btn btn-secondary disabled",
),
],
className="text-center my-4",
)
if authenticated and has_active_subscription:
label, href = "Gérer mon abonnement", "/compte/abonnement"
elif authenticated:
label, href = "Je m'abonne", "/compte/abonnement/mes-infos"
else:
label, href = "Je m'abonne", "/inscription"
return html.Div(
html.A(label, href=href, className="btn btn-primary btn-lg"),
className="text-center my-4",
)
3c. Dans subscription_terms, supprimer le bloc « Fonctionnalités incluses »
(le html.H4("Fonctionnalités incluses"), les deux dcc.Markdown qui l'entourent
et l'insertion de abonnement_features). Garder abonnement_features défini au
niveau module (il sert à _explainer). Le reste des CGU est inchangé.
3d. Remplacer layout :
def layout(**_):
authenticated = current_user.is_authenticated
has_active = authenticated and sub_db.has_active_subscription(current_user.id)
body = html.Div(
[
_plan_cards(),
_explainer(),
_subscribe_button(authenticated, has_active, TOUS_ABONNES),
subscription_terms,
]
)
return apropos_shell("abonnement", body)
- Step 4 : Lancer les tests pour vérifier le succès
Run: uv run pytest tests/test_abonnement_public.py -v
Expected: PASS (6 tests)
- Step 5 : Commit
git add src/pages/a_propos/abonnement.py tests/test_abonnement_public.py
git commit -m "feat(abonnement): page publique avec cards et bouton Je m'abonne conditionnel"
Task 4 : /compte/abonnement non-abonné → bouton « M'abonner » / « Me réabonner »
La branche non-abonné n'affiche plus de cards mais un bouton dont le libellé dépend de
has_used_trial. On supprime les composants cards devenus inutilisés ici et on migre
les tests obsolètes.
Files:
- Modify:
src/pages/compte/abonnement.py - Modify:
tests/subscriptions/test_compte_abonnement.py
Interfaces:
-
Consumes:
db.has_used_trial(user_id) -> bool(existant). -
Produces:
_reabo_button(has_used_trial: bool) -> html.Div -
Step 1 : Écrire / migrer les tests
Dans tests/subscriptions/test_compte_abonnement.py, supprimer ces 4 tests
(devenus obsolètes : les cards et leurs boutons ont quitté ce module) :
test_plan_cards_present_when_no_subscription, test_plan_cards_no_trial_when_trial_used,
test_subscribe_buttons_disabled_when_tous_abonnes, test_subscribe_buttons_active_when_flag_off.
Ajouter :
def test_reabo_button_never_subscribed():
from src.pages.compte import abonnement as compte_abonnement
text = str(compte_abonnement._reabo_button(has_used_trial=False))
assert "M'abonner" in text
assert "Me réabonner" not in text
assert "href='/a-propos/abonnement'" in text
def test_reabo_button_former_subscriber():
from src.pages.compte import abonnement as compte_abonnement
text = str(compte_abonnement._reabo_button(has_used_trial=True))
assert "Me réabonner" in text
assert "href='/a-propos/abonnement'" in text
- Step 2 : Lancer les tests pour vérifier l'échec
Run: uv run pytest tests/subscriptions/test_compte_abonnement.py -v
Expected: FAIL (_reabo_button inexistant)
- Step 3 : Implémenter
Dans src/pages/compte/abonnement.py :
3a. Supprimer l'import from src.pages.a_propos.abonnement import abonnement_features.
3b. Supprimer les fonctions _plan_card, _plan_cards, _explainer (devenues
inutilisées ici — elles vivent désormais dans a_propos/abonnement.py).
3c. Ajouter :
def _reabo_button(has_used_trial: bool):
label = "Me réabonner" if has_used_trial else "M'abonner"
return html.Div(
[
html.P(
"Abonnez-vous pour accéder aux fonctionnalités réservées "
"aux abonné·es.",
className="mb-3",
),
html.A(
label,
href="/a-propos/abonnement",
className="btn btn-primary",
),
],
className="mb-4",
)
3d. Dans layout, la branche else (celle qui faisait
body.extend([_plan_cards(trial_used=trial_used), _explainer()])) devient :
else:
if row is not None and row["status"] == "expired":
body.append(
dbc.Alert(
"Votre abonnement a expiré.", color="warning", className="mb-4"
)
)
body.append(_reabo_button(db.has_used_trial(current_user.id)))
La ligne trial_used = db.has_used_trial(...) en tête de layout n'est plus utilisée
que… nulle part → la supprimer (lignes ~290-292 : la variable trial_used).
- Step 4 : Lancer les tests pour vérifier le succès
Run: uv run pytest tests/subscriptions/test_compte_abonnement.py -v
Expected: PASS (les tests _active_view, _tous_abonnes_banner, _show_active_view + les 2 nouveaux)
- Step 5 : Vérifier l'absence de régression d'import
Run: uv run pytest tests/test_compte_pages.py tests/subscriptions/ -v
Expected: PASS (aucun module ne référence plus compte.abonnement._plan_cards)
- Step 6 : Commit
git add src/pages/compte/abonnement.py tests/subscriptions/test_compte_abonnement.py
git commit -m "feat(abonnement): /compte/abonnement non-abonné affiche M'abonner/Me réabonner"
Task 5 : CTA de /connexion vers l'offre d'abonnement
Le bouton du bas de /connexion pointe vers /a-propos/abonnement au lieu de
/inscription.
Files:
-
Modify:
src/pages/connexion.py(bloc final dulayout, lignes 91-96) -
Test:
tests/test_connexion_page.py(créer) -
Step 1 : Écrire le test qui échoue
# tests/test_connexion_page.py
def test_connexion_cta_points_to_abonnement():
from src.pages import connexion
text = str(connexion.layout())
assert "/a-propos/abonnement" in text
assert "Voir les abonnements" in text
- Step 2 : Lancer le test pour vérifier l'échec
Run: uv run pytest tests/test_connexion_page.py -v
Expected: FAIL (le CTA pointe encore vers /inscription)
- Step 3 : Implémenter
Dans src/pages/connexion.py, remplacer le dernier bloc du layout (le
html.A("Créer un compte avec mon adresse email", href="/inscription", ...)) par :
html.A(
"Pas encore de compte ? Voir les abonnements",
href="/a-propos/abonnement",
className="btn btn-primary w-100",
),
- Step 4 : Lancer le test pour vérifier le succès
Run: uv run pytest tests/test_connexion_page.py -v
Expected: PASS
- Step 5 : Commit
git add src/pages/connexion.py tests/test_connexion_page.py
git commit -m "feat(abonnement): CTA connexion vers /a-propos/abonnement"
Task 6 : Sélection de la formule par cartes cliquables dans mes-infos
Le formulaire mes-infos n'exige plus de paramètre ?plan=. La formule se choisit en
cliquant l'une des deux cartes (réutilisées de la page publique). Aucune sélection par
défaut : un texte invite l'utilisateur ; la carte choisie prend un fond teinté. Un
callback recopie la valeur dans un champ caché natif soumis dans le POST.
Files:
- Modify:
src/pages/compte/abonnement_mes_infos.py - Modify:
src/assets/css/style.css - Test:
tests/subscriptions/test_mes_infos_plan.py(créer)
Interfaces:
-
Consumes:
_plan_card(Task 3, depuissrc.pages.a_propos.abonnement),plans.plan_meta,plans.trial_days,sub_db.has_used_trial. -
Produces:
_selectable_cards(trial_for) -> dbc.Row_selection_state(selected: str) -> tuple[str, str, str, str](valeur cachée, classe carte simple, classe carte soutien, classe invite) — pur, testable- callback
_select_plan(déclenché parn_clicksdes cartes viactx.triggered_id) _toggle_submit(retractation, cgu, plan) -> bool(désactive tant qu'une case ou la formule manque)
-
Step 1 : Écrire les tests qui échouent
# tests/subscriptions/test_mes_infos_plan.py
import pytest
@pytest.fixture(autouse=True)
def _plan_env(monkeypatch):
monkeypatch.setenv("FRISBII_PLAN_SIMPLE", "plan_simple")
monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien")
def test_selectable_cards_render_both_plans():
from src.pages.compte import abonnement_mes_infos as m
text = str(m._selectable_cards(trial_for=lambda key: 2))
assert "plan-card-simple" in text
assert "plan-card-soutien" in text
assert "plan-selectable" in text
assert "Abonnement de soutien" in text
def test_selection_state_simple():
from src.pages.compte import abonnement_mes_infos as m
value, cls_simple, cls_soutien, cls_invite = m._selection_state("simple")
assert value == "simple"
assert "selected" in cls_simple
assert "selected" not in cls_soutien
assert cls_invite == "d-none"
def test_selection_state_soutien():
from src.pages.compte import abonnement_mes_infos as m
value, cls_simple, cls_soutien, _ = m._selection_state("soutien")
assert value == "soutien"
assert "selected" in cls_soutien
assert "selected" not in cls_simple
def test_submit_disabled_without_plan():
from src.pages.compte import abonnement_mes_infos as m
assert m._toggle_submit(["ok"], ["ok"], "") is True
assert m._toggle_submit(["ok"], ["ok"], "simple") is False
assert m._toggle_submit([], ["ok"], "simple") is True
- Step 2 : Lancer les tests pour vérifier l'échec
Run: uv run pytest tests/subscriptions/test_mes_infos_plan.py -v
Expected: FAIL (_selectable_cards / _selection_state inexistants)
- Step 3 : Implémenter
3a. Dans src/pages/compte/abonnement_mes_infos.py, compléter les imports :
ajouter ctx à l'import from dash import ..., ajouter _plan_card à l'import
existant depuis src.pages.a_propos.abonnement, et ajouter :
from src.subscriptions import db as sub_db
from src.subscriptions import plans
3b. Ajouter les helpers + callback de sélection (par ex. juste après _csrf_input) :
def _trial_for(user_id):
used = sub_db.has_used_trial(user_id)
return lambda key: None if used else plans.trial_days(key)
def _selectable_cards(trial_for):
cols = []
for key in ("simple", "soutien"):
meta = plans.plan_meta(key)
if not meta:
continue
cols.append(
dbc.Col(
html.Div(
_plan_card(meta, trial_for(key)),
id=f"plan-card-{key}",
n_clicks=0,
className="plan-selectable",
),
md=6,
)
)
return dbc.Row(cols, className="g-4 mb-2")
def _selection_state(selected):
base = "plan-selectable"
return (
selected,
f"{base} selected" if selected == "simple" else base,
f"{base} selected" if selected == "soutien" else base,
"d-none",
)
@callback(
Output("inf-plan-hidden", "value"),
Output("plan-card-simple", "className"),
Output("plan-card-soutien", "className"),
Output("inf-plan-invite", "className"),
Input("plan-card-simple", "n_clicks"),
Input("plan-card-soutien", "n_clicks"),
prevent_initial_call=True,
)
def _select_plan(_n_simple, _n_soutien):
selected = "simple" if ctx.triggered_id == "plan-card-simple" else "soutien"
return _selection_state(selected)
3c. Modifier le callback _toggle_submit existant pour exiger aussi une formule :
@callback(
Output("inf-submit", "disabled"),
Input("inf-cb-retractation", "value"),
Input("inf-cb-cgu", "value"),
Input("inf-plan-hidden", "value"),
)
def _toggle_submit(retractation, cgu, plan):
return not (retractation and cgu and plan)
3d. Dans layout, supprimer la garde ?plan= (lignes 53-55) :
plan = query.get("plan", "")
if not plan:
return dcc.Location(href="/compte/abonnement", id="inf-no-plan-redirect")
3e. Dans le html.Form (children=), remplacer la ligne
dcc.Input(type="hidden", name="plan", value=plan), par l'invite, les cartes
sélectionnables et le champ caché vide :
children=[
_csrf_input(),
html.Div(
"Choisissez votre formule :",
id="inf-plan-invite",
className="fw-bold mb-2",
),
_selectable_cards(_trial_for(current_user.id)),
dcc.Input(type="hidden", id="inf-plan-hidden", name="plan", value=""),
dbc.Row([col1, col2], className="g-4 mb-4"),
checkboxes,
html.Button(
"Ajout d'une carte de paiement",
id="inf-submit",
type="submit",
className="btn btn-primary",
disabled=True,
),
],
3f. Ajouter à la fin de src/assets/css/style.css :
.plan-selectable {
cursor: pointer;
}
.plan-selectable .card {
transition: background-color 0.15s, border-color 0.15s;
}
.plan-selectable.selected .card {
background-color: var(--bs-primary-bg-subtle);
border-color: var(--bs-primary);
}
- Step 4 : Lancer les tests pour vérifier le succès
Run: uv run pytest tests/subscriptions/test_mes_infos_plan.py -v
Expected: PASS (4 tests)
- Step 5 : Vérifier qu'aucun test existant ne dépendait de la garde
?plan=
Run: uv run pytest tests/subscriptions/ tests/test_page_loads.py -v
Expected: PASS
- Step 6 : Commit
git add src/pages/compte/abonnement_mes_infos.py src/assets/css/style.css tests/subscriptions/test_mes_infos_plan.py
git commit -m "feat(abonnement): sélection de formule par cartes cliquables dans mes-infos"
Task 7 : Vérification de bout en bout
Files: aucun (validation).
- Step 1 : Suite complète
Run: uv run pytest
Expected: PASS (aucune régression). Si des tests Selenium échouent faute de Chrome,
les relever mais valider au moins les tests unitaires et de routes touchés.
- Step 2 : Revue manuelle du parcours (skill
verifyrecommandé)
Dérouler : /a-propos/abonnement (déconnecté) → « Je m'abonne » → /inscription →
signup → lien email → auto-login → mes-infos (radios présents, défaut « simple »)
→ soumission → checkout. Puis, connecté sans abonnement : /compte/abonnement
montre « M'abonner ».
Self-Review (auteur)
Couverture spec :
- §1 page publique cards/explainer/bouton/trim CGU → Task 3 ✓
- §2 bouton conditionnel (4 états) → Task 3 (
_subscribe_button) ✓ - §3
/compte/abonnementM'abonner/Me réabonner viahas_used_trial→ Task 4 ✓ - §4 mes-infos cartes cliquables + gating submit + suppression garde
?plan=→ Task 6 ✓ - §5
verify_emailauto-login → Task 2 ✓ - §6 CTA connexion → Task 5 ✓
- §7
linkedin_button(next)+ inscription → Task 1 ✓
Placeholders : aucun ; tout le code est fourni.
Cohérence des types : _subscribe_button(bool, bool, bool), _reabo_button(bool),
_plan_cards(trial_for) / _plan_card(meta, trial) (Task 3, réutilisé par Task 6),
_selectable_cards(trial_for) -> dbc.Row, _selection_state(str) -> tuple[str,str,str,str],
_toggle_submit(retractation, cgu, plan) -> bool — noms et signatures cohérents entre
tâches et tests.