Commit Graph

269 Commits

Author SHA1 Message Date
Colin Maudry ee214677bd feat(abonnement): bouton changer de méthode de paiement sur /compte/abonnement
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 17:03:28 +02:00
Colin Maudry 081811bea7 feat(abonnement): route /subscriptions/change-payment-method 2026-07-05 16:52:16 +02:00
Colin Maudry 1cfb3e91e2 feat(abonnement): client.get_payment_info_url pour le changement de carte Frisbii 2026-07-05 16:46:38 +02:00
Colin Maudry 349fb17c90 feat(abonnement): sélection de formule par cartes cliquables dans mes-infos
Remplace la garde ?plan= (redirection si absent) par une sélection
interactive : deux cartes de formule (réutilisées de la page publique via
_plan_card) cliquables, un callback recopie le choix dans un champ caché
natif soumis avec le POST du formulaire. Le bouton de soumission reste
désactivé tant qu'aucune formule n'est choisie (en plus des cases à
cocher existantes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:43:39 +02:00
Colin Maudry 315c500005 feat(abonnement): CTA connexion vers /a-propos/abonnement
Remplace le CTA du bas de /connexion ("Créer un compte avec mon adresse
email" → "/inscription") par un lien vers l'offre d'abonnement ("Pas encore
de compte ? Voir les abonnements" → "/a-propos/abonnement").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:39:06 +02:00
Colin Maudry 3b744d87f0 feat(abonnement): /compte/abonnement non-abonné affiche M'abonner/Me réabonner 2026-07-04 23:34:35 +02:00
Colin Maudry c5c63cb68e fix(abonnement): calcul du prix TTC robuste (évite les artefacts flottants)
Remplace le hack `str(int(prix_ht) * 1.2).replace('.0', '')` par
`round(prix_ht * 1.2, 2)` formaté avec `:g`, qui gère correctement les
prix qui ne multiplient pas proprement par 1.2 (ex: 24 -> 28.8 au lieu
de 28.799999999999997).

Le fichier src/pages/compte/abonnement.py contient le même bug mais
n'est pas touché ici : sa copie de _plan_card sera supprimée à la
tâche 4 du plan de refonte du tunnel d'abonnement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:28:14 +02:00
Colin Maudry e904ec4a7d feat(abonnement): page publique avec cards et bouton Je m'abonne conditionnel
La page publique /a-propos/abonnement affiche désormais les cards de
formule (informatives, sans bouton par card), un explainer des
fonctionnalités, et un unique CTA "Je m'abonne" dont la cible dépend
de l'état d'authentification/abonnement. Retrait de la sous-section
"Fonctionnalités incluses" des CGU (doublon avec l'explainer).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:21:19 +02:00
Colin Maudry 6464baebd2 feat(auth): auto-login à la validation d'email, redirection vers mes-infos
Quand l'utilisateur valide son email, il est maintenant connecté automatiquement
via login_user() et redirigé directement vers /compte/abonnement/mes-infos
au lieu de /connexion?verified=1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:16:29 +02:00
Colin Maudry cc2b34e84a feat(abonnement): linkedin_button paramétrable par next, inscription vers mes-infos
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:13:11 +02:00
Colin Maudry 1551faaf0c Normaliser les échecs d'écriture set_cell en ValueError
- IntegrityError (ex: email déjà utilisé) est désormais capturée et
  reconvertie en ValueError, pour rester dans le funnel d'alerte
  existant du callback admin au lieu de faire planter le callback Dash.
- Une UPDATE qui touche 0 ligne (ligne supprimée entre le chargement du
  tableau et la soumission de l'édition) lève désormais une ValueError
  au lieu d'être silencieusement traitée comme un succès (ce qui aurait
  créé un log d'audit trompeur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:10:32 +02:00
Colin Maudry eb8f54dbf2 fix(figures): calculer center/zoom côté serveur au lieu d'utiliser bounds
La prop `bounds` de dash-leaflet lève une exception JS (TypeError sur
.equals() avec une valeur précédente indéfinie) au tout premier montage
du composant Map — confirmé via la stack trace navigateur, reproductible
même en dernière version (1.1.3). Cette exception laissait le clustering
dans un état incohérent (declustering impossible avant un zoom complet
suivi d'un dézoom).

bounds_to_center_zoom calcule un center/zoom approximatifs (projection
Web Mercator, taille de conteneur supposée) pour cadrer l'organisme et
ses contreparties, en évitant complètement la prop `bounds` bugguée.
Cadrage moins précis qu'un vrai fitBounds() navigateur, mais robuste.
2026-07-03 22:54:21 +02:00
Colin Maudry 6739e4926b test(admin): add end-to-end Selenium coverage for the generic table editor
Covers anonymous/non-admin access (404), and the full admin flow: switch
table, edit a subscriptions.prix_ht cell through the real DataTable UI,
verify the write lands in the DB, and verify the edit is logged and
visible in the admin_actions table.

tests/users.test.sqlite is committed and shared by the whole Selenium
session, so _cleanup_user deletes every row the tests create. Beyond
resetting sqlite_sequence (already needed for the AUTOINCREMENT
counter), a plain DELETE also leaves stale, never-zeroed bytes behind
in the admin_actions/subscriptions b-tree pages once they go back to
zero rows, which byte-diffs the file even though its logical content
is unchanged. VACUUM rebuilds the file from live data only, producing
a deterministic page layout (verified empirically across independent
runs with different random test data). Re-baselined the fixture to
that canonical vacuumed state so `git status` stays clean after
running the suite.
2026-07-03 22:43:53 +02:00
Colin Maudry e763a0b878 Taille du point de l'org 2026-07-03 22:29:04 +02:00
Colin Maudry f1c28acc96 feat(figures): mettre en avant le point de l'organisme consulté sur la carte
Marque is_home=True sur les marqueurs du home_type (acheteur ou titulaire
selon la page) dans get_org_location_map. Côté client, pointToLayer donne
à ce point un rayon légèrement plus grand (8 vs 5) et le ramène toujours
au premier plan (bringToFront), indépendamment de l'ordre des couches.
2026-07-03 22:08:43 +02:00
Colin Maudry 8b69fee31c feat(figures): ajouter get_org_location_map (carte cluster organisme + contrepartie) 2026-07-03 15:59:47 +02:00
Colin Maudry 21d3a30d8b refactor(figures): extraite build_org_markers pour réutilisation 2026-07-03 15:55:29 +02:00
Colin Maudry caf800e5e8 fix(admin): guard find_changed_cell against cross-table schema mismatch
When switching tables, the table-switch branch writes fresh data for the
new table, which re-fires the same callback with data_previous still
holding the old table's rows. find_changed_cell only checked row count
before diffing, so if the two tables happened to have the same number of
rows it would zip mismatched-schema dicts and report a spurious changed
cell (usually the PK column), producing a confusing red alert right after
switching tables.
2026-07-03 14:46:12 +02:00
Colin Maudry d229b58f3b feat(admin): replace dedicated pages with a generic table editor at /admin 2026-07-03 14:19:01 +02:00
Colin Maudry cf23863a30 refactor(admin): apply formatting fixes from linter hooks 2026-07-03 13:04:15 +02:00
Colin Maudry 348da79175 test(admin): verify login succeeded in non-admin 404 test
test_admin_non_admin_gets_404 asserted the same /admin 404 that
anonymous visitors also get, without first confirming the login
actually went through. A broken login (falls back to /connexion on
bad credentials, unverified email, etc.) would leave the session
anonymous and the test would keep passing for the wrong reason,
silently degrading into a duplicate of test_admin_anonymous_gets_404.
Now waits for the post-login redirect to /compte/abonnement (this
user has no subscription) before exercising the admin guard.
2026-07-03 10:44:31 +02:00
Colin Maudry 7cc1fadf0f test(admin): add end-to-end Selenium coverage for the admin panel
Covers anonymous → 404, non-admin → 404, and the full admin flow
(list, detail, status change, journal) through a real login and a
real running app. tests/users.test.sqlite is committed and shared by
the whole Selenium session, so the test cleanup also resets the
sqlite_sequence high-water marks that plain DELETEs don't roll back,
keeping the file byte-stable across runs. This run additionally bakes
in migration 0006_create_admin_actions (new admin_actions table), the
first time any Selenium test has booted the real app since that
migration was added — the same one-time process by which migrations
0001-0005 already ended up committed in this fixture.
2026-07-03 10:34:35 +02:00
Colin Maudry e483d7af4d feat(admin): add subscription-status mutation route
Wires is_admin(), SUBSCRIPTION_STATUSES/get_current/set_status, and
log_action() into POST /admin/actions/subscription-status: validates
the requested status and that subscription_id matches the user's
current subscription, applies the change, and logs an audit entry.
Registers the admin blueprint in init_auth() and documents ADMIN_EMAIL
in .template.env.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:46:22 +02:00
Colin Maudry 2f2cd151fb feat(admin): add is_admin() access guard
Implement access control function for admin panel. Returns True only if
ADMIN_EMAIL env var is set, user is authenticated, and email matches
case-insensitively.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:41:57 +02:00
Colin Maudry c4851ff0ae refactor: format test_db.py for consistency
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:38:46 +02:00
Colin Maudry fb62c28e10 feat(admin): add admin_actions audit table and log/list functions
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:38:33 +02:00
Colin Maudry 0209ec0d5c feat(admin): add subscription history, status override, and statuses constant
Adds four new DB helpers for the admin panel:
- SUBSCRIPTION_STATUSES: tuple of valid subscription statuses
- list_by_user(): retrieve all subscriptions for a user (newest first)
- set_status(): override a subscription's status and updated_at timestamp
- get_subscriber_state(): public wrapper for internal _get_state()

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:34:09 +02:00
Colin Maudry 9ea8bee940 feat(admin): add list_users() to auth DB layer
Implements list_users(limit: int = 1000) -> list[sqlite3.Row] in the auth
DB layer to retrieve all users ordered by creation date (most recent first).

- Returns all users, optionally capped at limit (default 1000)
- Orders by created_at DESC to show newest users first
- Follows existing DB layer patterns using get_conn().execute().fetchall()

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 09:29:03 +02:00
Colin Maudry 0b88a65414 Utiliser le Checkout API pour la session de souscription (accept_url/cancel_url)
hosted_page_links.payment_info (POST /v1/subscription) n'honore pas
accept_url/cancel_url malgré la doc, même passés en query string (constaté
en test sur test.colibre.fr). On génère maintenant la page de paiement via
POST /v1/session/subscription (Checkout API), qui accepte ces champs dans
son body et redirige effectivement le navigateur — même mécanisme déjà
fonctionnel pour l'ajout de moyen de paiement (create_recurring_session).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 18:21:05 +02:00
Colin Maudry 14ae1a7bfb Gérer l'affichage d'un abonnement expiré (statut Frisbii "expired")
_show_active_view affichait par erreur la vue "abonnement actif" (avec un
faux "Prochaine facturation") pour un abonnement expiré. Un abonnement
expiré bascule maintenant sur l'écran de re-souscription, avec une alerte
dédiée. Jamais remarqué jusqu'ici car les webhooks Frisbii ne remontaient
pas ce changement de statut.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 18:13:12 +02:00
Colin Maudry 1159697c0e 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>
2026-07-02 17:56:19 +02:00
Colin Maudry d08f7aeeac Nombre de jours d'essai en dur plutot que via appels HTTP 2026-07-02 13:55:22 +02:00
Colin Maudry 1bdcaaf5d8 Réactivation de l'env MAIL_SUPPRESS_SEND 2026-07-02 13:42:05 +02:00
Colin Maudry cd107a0213 Refactor structure modules pages/compte, petits changements de texte 2026-07-02 10:53:01 +02:00
Colin Maudry 9ff2373ccb decp.info => colibre 2026-07-01 18:00:36 +02:00
Colin Maudry 126834c7d4 fix(subscriptions): un abonnement en échec (failed) ne bloque plus le réabonnement 2026-07-01 16:56:34 +02:00
Colin Maudry 872fc33dd7 feat(subscriptions): cancel/webhook/add_payment_callback sur get_current et customer_known 2026-07-01 16:41:57 +02:00
Colin Maudry 0ffe647657 feat(subscriptions): subscribe() génère et transmet le handle, mark_failed en cas d'échec 2026-07-01 14:37:25 +02:00
Colin Maudry 179e6e436d feat(subscriptions): create_subscription_session prend le handle en paramètre 2026-07-01 14:34:09 +02:00
Colin Maudry 64158e9080 feat(subscriptions): historique multi-lignes + subscriber_state + handle abo-{user_id}-N
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 14:29:47 +02:00
Colin Maudry a2c20adb4e perf: paginer/agréger les pages acheteur et titulaire côté DuckDB
Les pages acheteur/titulaire chargeaient l'intégralité des marchés d'une
organisation dans un dcc.Store côté client (jusqu'à 16k lignes pour les
plus gros acheteurs), envoyée sur le réseau à chaque interaction. Le
tableau, le top 10 et l'histogramme des distances récupèrent maintenant
leurs données via des requêtes DuckDB scopées (pagination/agrégation
poussées en SQL), sur le modèle déjà utilisé par la page tableau.

Corrige aussi le CLS des mêmes pages en réservant l'espace des
conteneurs remplis par callback (carte, top 10, histogramme).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 13:46:44 +02:00
Colin Maudry 8515ba8d84 refactor: replace all decpinfo with colibre in test suite
Update all client and fixture references throughout the test suite
to use colibre branding instead of the legacy decpinfo naming.

- test_auth.py: token format
- benchmark.py: token format documentation
- test_routes.py: customer handles
- test_client.py: customer identifiers
- test_db.py: all database fixtures

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-01 11:19:25 +02:00
Colin Maudry 190b8154b4 refactor: replace decpinfo with colibre in test client identifiers
Update test fixtures and client references to use colibre branding
instead of the legacy decpinfo naming convention.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-01 11:17:18 +02:00
Colin Maudry 7184b7768a feat(seo): index de sitemaps acheteurs/titulaires, canonical, robots
- robots.txt déclare désormais le sitemap
- /sitemap.xml devient un index de sous-sitemaps paginés (limite 50k URLs/fichier)
  générés depuis DuckDB et mis en cache 24h : pages statiques, acheteurs, titulaires
- balise canonical auto-référente (JS, car index_string Dash partagé)
- config nginx de référence : 301 decp.info → colibre.fr (chemin préservé)
- tests SEO via test client Flask

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:06:46 +02:00
Colin Maudry 64d4821fae fix(tests): met à jour le préfixe de token decpinfo_ → colibre_ #57
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:49:51 +02:00
Colin Maudry 9b124deeea refactor: rebrand decp.info to colibre #57
- Rename project from decp.info to colibre across all codebase
- Update domain from https://decp.info to https://colibre.fr
- Update GitHub repo references to ColinMaudry/colibre
- Rename deployment files: decpinfo-backup.* → colibre-backup.*
- Update project configuration and documentation
- Rename project assets: decp.info.png → colibre.png
- Update environment variables and constants (DOMAIN_NAME, TOKEN_PREFIX, GITHUB_REPO, etc.)
- Update URLs in all pages, tests, and configuration files
- Keep DECP acronym in text (unchanged per requirements)
- Add rebrand note to README.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-30 21:43:33 +02:00
Colin Maudry d8faccab46 fix: corrige bug build_database + fiabilise et isole la suite de tests
src/db.py : le remplacement des noms d'organisation nuls utilisait
.name.keep() sur une expression concat_str référençant *_id ; Polars nommait
alors le résultat d'après *_id, écrasant la colonne titulaire_id/acheteur_id et
laissant *_nom inchangée. Remplacé par .alias(col).

Suite de tests — élimination de la pollution inter-tests (callbacks/conn
globaux Dash) qui faisait échouer ~15 tests Selenium en exécution complète :
- tests/conftest.py : DUCKDB_PATH et DATA_SCHEMA_CACHE pointent sur des chemins
  de test isolés (tests/decp.duckdb, tests/schema.cache.json, gitignorés) — on
  ne touche plus jamais aux fichiers versionnés decp.duckdb et
  schema.fixture.json (qui étaient mutés et cassaient la collecte au run
  suivant). Viewport Chrome élargi à 1600px.
- tests/test_db.py : fixture module-scoped qui recharge src.db après le module
  (test_query_marches faisait importlib.reload vers une DB temporaire au schéma
  réduit, polluant le conn global → ColumnNotFoundError ensuite). Assertion mise
  à jour pour le nouveau libellé "[Inconnu de l'INSEE (...)]".

Tests périmés / fragiles corrigés :
- tests/auth/test_oauth_routes.py : mock LinkedIn aligné sur la route (userinfo
  récupéré via .get() séparé) ; destination post-login /compte/abonnement.
- tests/test_main.py : test_002 filtre sur dateNotification (uid retiré du
  tableau), scrollIntoView (colonne hors viewport), attentes de l'application du
  filtre et de la restauration de la persistance ; test_015 via
  wait_for_no_elements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:22:27 +02:00
Colin Maudry c4aa90b0d1 feat: améliorations UX roadmap votes #94
- Votes cappés à VOTES_PER_WEEK (pas d'accumulation) pour valoriser
  les connexions régulières
- Solde de votes affiché dans la liste (input inéditable) à la place
  du bandeau Alert, avec date de prochain rechargement
- Compteur de votes par feature affiché comme input inéditable en
  fin de ligne (avant le bouton "+")
- Animation FLIP JS (roadmap_flip.js) : seules les lignes qui changent
  de position sont animées après un vote
- Renommage votes_credited_until → votes_last_credited_at (migration 0005)
- Constantes du module utilisées dans les assertions de tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 16:39:13 +02:00
Colin Maudry 9bed4e3fb9 feat: page publique /a-propos/roadmap (lecture seule) #94 2026-06-30 14:25:59 +02:00
Colin Maudry 8161a8b8b4 fix: test_visible_sections vérifie roadmap au lieu d'archives #94
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 14:20:51 +02:00