diff --git a/config/application.rb b/config/application.rb
index 8da3c590..f081f699 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -17,6 +17,8 @@ class Application < Rails::Application
config.i18n.default_locale = :fr
config.i18n.available_locales = %i[fr en]
+ # Until en.yml mirrors all fr keys, :en lookups fall back to French copy.
+ config.i18n.fallbacks = { en: %i[fr] }
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 02d0f52b..16823968 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -61,7 +61,7 @@
config.active_job.verbose_enqueue_logs = true
# Raises error for missing translations.
- # config.i18n.raise_on_missing_translations = true
+ config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
config.action_view.annotate_rendered_view_with_filenames = true
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6c349ae5..654374a1 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,31 +1,400 @@
-# Files in the config/locales directory are used for internationalization and
-# are automatically loaded by Rails. If you want to use locales other than
-# English, add the necessary files in this directory.
-#
-# To use the locales, use `I18n.t`:
-#
-# I18n.t "hello"
-#
-# In views, this is aliased to just `t`:
-#
-# <%= t("hello") %>
-#
-# To use a different locale, set it with `I18n.locale`:
-#
-# I18n.locale = :es
-#
-# This would use the information in config/locales/es.yml.
-#
-# To learn more about the API, please read the Rails Internationalization guide
-# at https://guides.rubyonrails.org/i18n.html.
-#
-# Be aware that YAML interprets the following case-insensitive strings as
-# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
-# must be quoted to be interpreted as strings. For example:
-#
-# en:
-# "yes": yup
-# enabled: "ON"
-
+---
en:
- hello: "Hello world"
+ flash:
+ generic:
+ error_detail: 'Error: %{message}'
+ account_claims:
+ create:
+ rescue_alert: 'Error: %{message}'
+ success: Demande de réclamation envoyée. Vérifiez vos emails.
+ confirm:
+ rescue_alert: 'Error while claiming account: %{message}'
+ success: "✅ Compte revendiqué ! Votre historique est maintenant disponible."
+ admin:
+ memberships:
+ create:
+ upgrade_failed_alert: 'Error during upgrade: %{message}'
+ membership_creation_failed_alert: 'Error creating membership: %{message}'
+ upgrade_notice_offered: Membership upgraded successfully! %{name} — Complimentary
+ upgrade_notice_paid: 'Membership upgraded successfully! %{name} — Amount: %{amount}€'
+ upgrade_notice_member_number_suffix: " | Member number changed: %{old_number} → %{new_number}"
+ duplicate_active: Cette personne possède déjà une adhésion active.
+ success_with_contribution_hint: Adhésion créée avec succès ! Vous pouvez maintenant ajouter une cotisation depuis la fiche utilisateur.
+ update:
+ success: Adhésion mise à jour avec succès.
+ destroy:
+ deactivated: Adhésion désactivée avec succès.
+ payments:
+ create:
+ failure_alert: 'Error creating payment: %{message}'
+ created_notice: Paiement créé avec succès
+ update:
+ failure_alert: 'Update failed: %{message}'
+ success_notice: Mise à jour réussie
+ destroy:
+ cancel_failed_alert: 'Failed to cancel payment: %{message}'
+ cancelled_notice: Paiement annulé avec succès
+ restore:
+ restore_failed_alert: 'Failed to restore payment: %{message}'
+ restored_notice: Paiement restauré avec succès
+ show:
+ use_inline_notice: Utilisez l'édition inline pour modifier les paiements
+ new:
+ creation_disabled_notice: Création de paiement temporairement désactivée
+ subscription_plans:
+ create:
+ purchase_failed_alert: 'Error purchasing plan: %{message}'
+ purchased: Plan de cotisation acheté avec succès !
+ new:
+ needs_circus_membership_alert: Cette personne doit avoir une adhésion Cirque pour acheter des plans de cotisation
+ update:
+ updated: Plan de cotisation mis à jour avec succès !
+ destroy:
+ destroyed: Plan de cotisation supprimé avec succès !
+ require_super_admin:
+ forbidden: Seul le super-admin peut modifier ou supprimer des cotisations.
+ subscriptions:
+ upgrade:
+ success_notice: Contribution upgraded successfully.
+ credit_applied_suffix: " Credit applied: %{amount}€"
+ failure_alert: 'Error during upgrade: %{message}'
+ users:
+ create:
+ invalid_data_alert: 'Invalid data: %{details}'
+ success_person_user_membership: Person, web account and membership created successfully!
+ success_person_user: Person and web account created successfully!
+ success_person_membership: Person and membership created successfully!
+ success_person_only: Person created successfully!
+ error_existing_web_account: This person already has a web account.
+ error_email_required: An email is required to create a web account.
+ payments:
+ create:
+ failure_alert: 'Error creating payment: %{message}'
+ success: Paiement créé avec succès
+ update:
+ failure_alert: 'Error updating payment: %{message}'
+ success: Paiement mis à jour avec succès
+ destroy:
+ failure_alert: 'Error deleting payment: %{message}'
+ destroyed: Paiement supprimé avec succès
+ process_payment:
+ failure_alert: 'Error processing payment: %{message}'
+ processed: Paiement traité avec succès
+ already_processed: Paiement déjà traité
+ show:
+ merged_person_notice: Cette fiche a été fusionnée avec une autre. Retour à la liste des utilisateurs.
+ edit:
+ user_not_found: Utilisateur non trouvé.
+ edit_error: Une erreur est survenue lors de l'édition de l'utilisateur.
+ update:
+ person_saved_notice: Informations mises à jour avec succès.
+ ajax_success_json_message: Informations mises à jour avec succès.
+ html_updated: Utilisateur mis à jour avec succès.
+ turbo_notice: Utilisateur mis à jour avec succès.
+ destroy:
+ person_not_found_alert: Personne non trouvée.
+ person_deleted_notice: Personne supprimée avec succès.
+ destruction_failed_alert_html: "❌ %{message}"
+ user_archived_notice: Utilisateur archivé avec succès.
+ archive_failed_alert: Impossible d'archiver cet utilisateur.
+ restore:
+ restored_notice: Utilisateur restauré avec succès.
+ restore_failed_alert: Impossible de restaurer cet utilisateur.
+ set_user:
+ person_or_user_missing_alert: Utilisateur non trouvé.
+ check_deletion_permissions:
+ higher_privileges: Impossible de supprimer un utilisateur avec des privilèges égaux ou supérieurs.
+ require_super_admin:
+ restore_denied_alert: Seul le super-admin peut restaurer des utilisateurs.
+ base:
+ unauthorized_alert: Vous n'avez pas accès à cette page.
+ attendances:
+ create:
+ success: Présence enregistrée avec succès
+ destroy:
+ destroyed: Présence supprimée avec succès
+ failure: Erreur lors de la suppression
+ blogs:
+ create:
+ created: Blog créé avec succès
+ update:
+ updated: Blog mis à jour avec succès
+ destroy:
+ destroyed: Blog supprimé avec succès
+ donations:
+ create:
+ recorded: Donation prise en compte
+ events:
+ create:
+ created: Événement créé avec succès
+ update:
+ updated: Événement modifié avec succès
+ destroy:
+ destroyed_notice: Événement supprimé avec succès
+ membership_types:
+ create:
+ html_notice: Type d'adhésion créé avec succès !
+ turbo_notice: Type d'adhésion créé avec succès !
+ update:
+ html_notice: Type d'adhésion mis à jour avec succès !
+ turbo_notice: Type d'adhésion mis à jour avec succès !
+ destroy:
+ destroyed: Type d'adhésion supprimé avec succès !
+ notepads:
+ edit:
+ breadcrumb_modify: Modifier le bloc-note
+ update:
+ updated: Bloc-note mis à jour !
+ opening_hours:
+ update:
+ success: Horaires mis à jour avec succès
+ sessions:
+ rate_limited_alert: Rééssayez plus tard
+ create:
+ success: Connexion réussie !
+ invalid_credentials: Email ou mot de passe invalide
+ destroy:
+ signed_out: Déconnecté avec succès !
+ breadcrumbs:
+ admin:
+ common:
+ administration: Administration
+ dashboard: Dashboard
+ edit: Edit
+ users:
+ members_list: Members list
+ create_web_account: Create web account
+ new_member: New member
+ memberships:
+ management: Membership management
+ membership: Membership
+ upgrade_to_circus: Upgrade to Circus
+ new_membership: New membership
+ edit_membership: Edit membership
+ payments:
+ history: Payment history
+ management: Payments management
+ subscription_plans:
+ plans: Contribution plans
+ plan_named: 'Plan: %{name}'
+ new_contribution: New contribution
+ edit_named: 'Edit: %{name}'
+ opening_hours:
+ title: Opening hours
+ events:
+ events: Events
+ new_event: New event
+ attendances:
+ management: Attendance management
+ attendance_number: 'Attendance #%{id}'
+ new_attendance: New attendance
+ membership_types:
+ types: Membership types
+ type_named: 'Type: %{name}'
+ new_type: New membership type
+ edit_named: 'Edit: %{name}'
+ attendance_lists:
+ lists: Attendance lists
+ new_list: New list
+ health_reports:
+ integrity_report: Integrity report
+ helpers:
+ admin:
+ users:
+ display:
+ not_provided: Not provided
+ no_email: No email
+ name_not_provided: Name not provided
+ layouts:
+ application:
+ title: Le Circographe
+ views:
+ shared:
+ flash:
+ close: Close
+ navbar:
+ open_menu: Open menu
+ place: The Place
+ circus: Circus
+ graphic_arts: Graphic Arts
+ how_it_works: How it works
+ activities: Our Activities
+ join: Join
+ about: About
+ contact: Contact us
+ faq: FAQ
+ my_space: My Space
+ user: User
+ profile: Profile
+ dashboard: Dashboard
+ settings: Settings
+ logout: Sign out
+ sign_in: Sign in
+ sign_up: Sign up
+ association: Association
+ news: News
+ our_news: Our news
+ blog_newsletters: Blog & Newsletters
+ photo_gallery: Photo gallery
+ events: Our Events
+ footer:
+ newsletter_cta: Stay informed by subscribing to our newsletter
+ email_placeholder: Your email address
+ sign_up: Sign up
+ discover: Discover
+ about: About
+ place: The Place
+ activities: Our activities
+ participate: Participate
+ join: Join
+ contact: Contact us
+ map: Map
+ resources: Resources
+ faq: FAQ
+ terms: Legal notice
+ privacy: Privacy policy
+ brand: Le Circographe
+ all_rights_reserved: All rights reserved.
+ admin:
+ users:
+ new:
+ page_title: Create a new user
+ page_subtitle: Add a new user to the system
+ user_information: User information
+ create_web_account: Create a web account
+ create_web_account_help: Allow this person to sign in and manage their online profile
+ system_role: System role
+ roles:
+ web_visitor: Web visitor
+ volunteer: Volunteer
+ admin: Administrator
+ temporary_password_notice: A temporary password will be generated and sent by email
+ index:
+ page_title: Members Management
+ page_subtitle: Unified interface to manage people, memberships and contributions
+ statistics: Statistics
+ total_people: Total people
+ new_yesterday: New (yesterday)
+ basic_memberships: Basic memberships
+ circus_memberships: Circus memberships
+ active_memberships: Active memberships
+ without_account: Without account
+ create_member: Create a Member
+ pages:
+ about:
+ page_title: Le Circographe - About
+ join_community: Join the community
+ heading: About Le Circographe
+ hero_description: A laboratory of shared artistic practices where circus and graphic arts meet — self-management and solidarity.
+ join: Join us
+ discover_place: Discover the place
+ our_mission: Our mission
+ mission_heading: A shared and accessible creative space
+ mission_text_1: Provide a space for practice, creation and transmission that is accessible to everyone.
+ mission_text_2: 'Self-managed governance: members volunteer, share responsibilities, and co-program the life of the venue.'
+ services:
+ validation:
+ invalid_data: Invalid data
+ invalid_data_with_details: 'Invalid data: %{details}'
+ web:
+ user_registration:
+ accept_cgu: Vous devez accepter les CGU pour continuer.
+ accept_privacy: Vous devez accepter la politique de confidentialité pour continuer.
+ checkout:
+ success:
+ payment_ok: Paiement réussi et commande mise à jour.
+ payment_failed_alert: Paiement non réussi, commande non mise à jour.
+ cancel:
+ cancelled_alert: Le paiement a été annulé.
+ contacts:
+ create:
+ blank_fields: Veuillez remplir tous les champs du formulaire.
+ sent_notice: Votre message a été envoyé avec succès ! Nous revenons vers vous rapidement.
+ send_error: Une erreur est survenue lors de l'envoi. Réessaie dans quelques instants.
+ event_interests:
+ create:
+ profile_incomplete: Votre profil n'est pas complet. Veuillez contacter l'administration.
+ interest_added: Vous êtes maintenant intéressé par cet événement !
+ interest_error: Erreur lors de l'ajout de votre intérêt
+ destroy:
+ interest_removed: Vous n'êtes plus intéressé par cet événement
+ interest_remove_error: Erreur lors de la suppression de votre intérêt
+ passwords:
+ create:
+ reset_instructions_sent_generic: Instructions de réinitialisation du mot de passe envoyées (si un utilisateur avec cette adresse e-mail existe).
+ update:
+ password_reset_success: Le mot de passe a été réinitialisé.
+ request_reset:
+ sent_to_email_notice: Instructions de réinitialisation du mot de passe envoyées à %{email}.
+ must_be_signed_in_alert: Vous devez être connecté pour effectuer cette action.
+ invalid_token_alert: Le lien de réinitialisation du mot de passe est invalide ou a expiré.
+ registrations:
+ create:
+ success_notice: Inscription réussie !
+ sessions:
+ rate_limited_alert: Rééssayez plus tard
+ create:
+ login_success_notice: Connexion réussie !
+ invalid_credentials_flash: Email ou mot de passe invalide
+ invalid_credentials_redirect: Email ou mot de passe invalide
+ destroy:
+ signed_out_notice: Déconnecté avec succès !
+ settings:
+ update:
+ saved_notice: Vos modifications ont été enregistrées avec succès
+ users:
+ update:
+ profile_updated: Votre profil a été mis à jour avec succès.
+ destroy:
+ deleted_notice: Votre compte a été supprimé avec succès.
+ destroy_failed_alert: Impossible de supprimer votre compte. Veuillez contacter l'assistance.
+ change_newsletter_status:
+ redirect_manage_in_settings_alert: Gérez votre newsletter depuis vos paramètres.
+ newsletter_signup:
+ honeypot_thanks_notice: Merci pour votre inscription!
+ email_blank_alert: Veuillez entrer une adresse email valide.
+ manage_newsletter_notice: Gérez votre newsletter depuis vos paramètres en cochant/décochant la case.
+ unsubscribe_by_token:
+ invalid_token_alert: Token de désinscription invalide.
+ mailers:
+ account_claim_mailer:
+ confirmation_email:
+ subject: Confirmez votre revendication de compte - Le Circographe
+ passwords_mailer:
+ reset:
+ subject: Réinitialisez votre mot de passe
+ user_mailer:
+ welcome_by_admin:
+ subject: 'Bienvenue au Circographe ! '
+ welcome_email:
+ subject: 'Bienvenue au Circographe ! '
+ membership_expiration_reminder:
+ subject: Votre adhésion arrive à expiration !
+ activerecord:
+ errors:
+ models:
+ attendance:
+ attributes:
+ person_id:
+ event_interest_taken: est déjà intéressé par cet événement
+ daily_presence_taken: est déjà marqué présent aujourd'hui
+ payment_line:
+ attributes:
+ item_type:
+ not_allowed: "%{value} n'est pas un type d'article pris en charge"
+ user:
+ attributes:
+ cgu:
+ must_accept: Vous devez accepter les CGU pour continuer.
+ privacy_policy:
+ must_accept: Vous devez accepter la politique de confidentialité pour continuer.
+ payment_line:
+ item_description:
+ membership_fallback: Adhésion
+ membership_type_fallback: Type d'adhésion
+ contribution_fallback: Cotisation
+ donation_fallback: Donation
+ descriptions:
+ membership: Adhésion %{name}
+ contribution_formula: "%{name} (%{duration})"
+ membership_type: Adhésion %{name}
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 189b961d..f97de3a4 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1,22 +1,23 @@
-# French UI strings — Le Circographe (Rails/I18nLocaleTexts compliant)
-#
-# Controllers use lazy lookups under fr...* unless noted.
-#
+---
fr:
+ flash:
+ generic:
+ error_detail: 'Erreur: %{message}'
account_claims:
create:
- success: "Demande de réclamation envoyée. Vérifiez vos emails."
+ success: Demande de réclamation envoyée. Vérifiez vos emails.
+ rescue_alert: 'Erreur: %{message}'
confirm:
success: "✅ Compte revendiqué ! Votre historique est maintenant disponible."
-
+ rescue_alert: 'Erreur lors de la réclamation: %{message}'
activerecord:
errors:
models:
attendance:
attributes:
person_id:
- event_interest_taken: "est déjà intéressé par cet événement"
- daily_presence_taken: "est déjà marqué présent aujourd'hui"
+ event_interest_taken: est déjà intéressé par cet événement
+ daily_presence_taken: est déjà marqué présent aujourd'hui
payment_line:
attributes:
item_type:
@@ -24,235 +25,376 @@ fr:
user:
attributes:
cgu:
- must_accept: "Vous devez accepter les CGU pour continuer."
+ must_accept: Vous devez accepter les CGU pour continuer.
privacy_policy:
- must_accept: "Vous devez accepter la politique de confidentialité pour continuer."
-
+ must_accept: Vous devez accepter la politique de confidentialité pour continuer.
admin:
base:
- unauthorized_alert: "Vous n'avez pas accès à cette page."
-
+ unauthorized_alert: Vous n'avez pas accès à cette page.
attendances:
create:
- success: "Présence enregistrée avec succès"
+ success: Présence enregistrée avec succès
destroy:
- destroyed: "Présence supprimée avec succès"
- failure: "Erreur lors de la suppression"
-
+ destroyed: Présence supprimée avec succès
+ failure: Erreur lors de la suppression
blogs:
create:
- created: "Blog créé avec succès"
+ created: Blog créé avec succès
update:
- updated: "Blog mis à jour avec succès"
+ updated: Blog mis à jour avec succès
destroy:
- destroyed: "Blog supprimé avec succès"
-
+ destroyed: Blog supprimé avec succès
donations:
create:
- recorded: "Donation prise en compte"
-
+ recorded: Donation prise en compte
events:
create:
- created: "Événement créé avec succès"
+ created: Événement créé avec succès
update:
- updated: "Événement modifié avec succès"
+ updated: Événement modifié avec succès
destroy:
- destroyed_notice: "Événement supprimé avec succès"
-
+ destroyed_notice: Événement supprimé avec succès
membership_types:
create:
- html_notice: "Type d'adhésion créé avec succès !"
- turbo_notice: "Type d'adhésion créé avec succès !"
+ html_notice: Type d'adhésion créé avec succès !
+ turbo_notice: Type d'adhésion créé avec succès !
update:
- html_notice: "Type d'adhésion mis à jour avec succès !"
- turbo_notice: "Type d'adhésion mis à jour avec succès !"
+ html_notice: Type d'adhésion mis à jour avec succès !
+ turbo_notice: Type d'adhésion mis à jour avec succès !
destroy:
- destroyed: "Type d'adhésion supprimé avec succès !"
-
+ destroyed: Type d'adhésion supprimé avec succès !
memberships:
update:
- success: "Adhésion mise à jour avec succès."
+ success: Adhésion mise à jour avec succès.
destroy:
- deactivated: "Adhésion désactivée avec succès."
+ deactivated: Adhésion désactivée avec succès.
create:
- duplicate_active: "Cette personne possède déjà une adhésion active."
- success_with_contribution_hint: "Adhésion créée avec succès ! Vous pouvez maintenant ajouter une cotisation depuis la fiche utilisateur."
-
+ duplicate_active: Cette personne possède déjà une adhésion active.
+ success_with_contribution_hint: Adhésion créée avec succès ! Vous pouvez maintenant ajouter une cotisation depuis la fiche utilisateur.
+ upgrade_failed_alert: 'Erreur lors de l''upgrade: %{message}'
+ membership_creation_failed_alert: 'Erreur lors de la création de l''adhésion: %{message}'
+ upgrade_notice_offered: Adhésion upgradée avec succès ! %{name} - Offert
+ upgrade_notice_paid: 'Adhésion upgradée avec succès ! %{name} - Montant: %{amount}€'
+ upgrade_notice_member_number_suffix: " | Numéro d'adhérent changé: %{old_number} → %{new_number}"
notepads:
edit:
- breadcrumb_modify: "Modifier le bloc-note"
+ breadcrumb_modify: Modifier le bloc-note
update:
- updated: "Bloc-note mis à jour !"
-
+ updated: Bloc-note mis à jour !
opening_hours:
update:
- success: "Horaires mis à jour avec succès"
-
+ success: Horaires mis à jour avec succès
payments:
show:
- use_inline_notice: "Utilisez l'édition inline pour modifier les paiements"
+ use_inline_notice: Utilisez l'édition inline pour modifier les paiements
new:
- creation_disabled_notice: "Création de paiement temporairement désactivée"
+ creation_disabled_notice: Création de paiement temporairement désactivée
create:
- created_notice: "Paiement créé avec succès"
+ created_notice: Paiement créé avec succès
+ failure_alert: 'Erreur lors de la création du paiement: %{message}'
update:
- success_notice: "Mise à jour réussie"
+ success_notice: Mise à jour réussie
+ failure_alert: 'Échec de la mise à jour: %{message}'
destroy:
- cancelled_notice: "Paiement annulé avec succès"
+ cancelled_notice: Paiement annulé avec succès
+ cancel_failed_alert: 'Échec de l''annulation du paiement: %{message}'
restore:
- restored_notice: "Paiement restauré avec succès"
-
+ restored_notice: Paiement restauré avec succès
+ restore_failed_alert: 'Échec de la restauration du paiement: %{message}'
sessions:
- rate_limited_alert: "Rééssayez plus tard"
+ rate_limited_alert: Rééssayez plus tard
create:
- success: "Connexion réussie !"
- invalid_credentials: "Email ou mot de passe invalide"
+ success: Connexion réussie !
+ invalid_credentials: Email ou mot de passe invalide
destroy:
- signed_out: "Déconnecté avec succès !"
-
+ signed_out: Déconnecté avec succès !
subscription_plans:
new:
- needs_circus_membership_alert: "Cette personne doit avoir une adhésion Cirque pour acheter des plans de cotisation"
+ needs_circus_membership_alert: Cette personne doit avoir une adhésion Cirque pour acheter des plans de cotisation
create:
- purchased: "Plan de cotisation acheté avec succès !"
+ purchased: Plan de cotisation acheté avec succès !
+ purchase_failed_alert: 'Erreur lors de l''achat du plan: %{message}'
update:
- updated: "Plan de cotisation mis à jour avec succès !"
+ updated: Plan de cotisation mis à jour avec succès !
destroy:
- destroyed: "Plan de cotisation supprimé avec succès !"
+ destroyed: Plan de cotisation supprimé avec succès !
require_super_admin:
- forbidden: "Seul le super-admin peut modifier ou supprimer des cotisations."
-
+ forbidden: Seul le super-admin peut modifier ou supprimer des cotisations.
+ subscriptions:
+ upgrade:
+ success_notice: Cotisation upgradée avec succès.
+ credit_applied_suffix: " Crédit appliqué: %{amount}€"
+ failure_alert: 'Erreur lors de l''upgrade: %{message}'
users:
+ create:
+ invalid_data_alert: 'Données invalides : %{details}'
+ success_person_user_membership: Personne, compte web et adhésion créés avec succès !
+ success_person_user: Personne et compte web créés avec succès !
+ success_person_membership: Personne et adhésion créées avec succès !
+ success_person_only: Personne créée avec succès !
+ error_existing_web_account: Cette personne a déjà un compte web.
+ error_email_required: Un email est obligatoire pour créer un compte web.
show:
- merged_person_notice: "Cette fiche a été fusionnée avec une autre. Retour à la liste des utilisateurs."
+ merged_person_notice: Cette fiche a été fusionnée avec une autre. Retour à la liste des utilisateurs.
edit:
- user_not_found: "Utilisateur non trouvé."
- edit_error: "Une erreur est survenue lors de l'édition de l'utilisateur."
+ user_not_found: Utilisateur non trouvé.
+ edit_error: Une erreur est survenue lors de l'édition de l'utilisateur.
update:
- person_saved_notice: "Informations mises à jour avec succès."
- ajax_success_json_message: "Informations mises à jour avec succès."
- html_updated: "Utilisateur mis à jour avec succès."
- turbo_notice: "Utilisateur mis à jour avec succès."
+ person_saved_notice: Informations mises à jour avec succès.
+ ajax_success_json_message: Informations mises à jour avec succès.
+ html_updated: Utilisateur mis à jour avec succès.
+ turbo_notice: Utilisateur mis à jour avec succès.
destroy:
- person_not_found_alert: "Personne non trouvée."
- person_deleted_notice: "Personne supprimée avec succès."
+ person_not_found_alert: Personne non trouvée.
+ person_deleted_notice: Personne supprimée avec succès.
destruction_failed_alert_html: "❌ %{message}"
- user_archived_notice: "Utilisateur archivé avec succès."
- archive_failed_alert: "Impossible d'archiver cet utilisateur."
+ user_archived_notice: Utilisateur archivé avec succès.
+ archive_failed_alert: Impossible d'archiver cet utilisateur.
restore:
- restored_notice: "Utilisateur restauré avec succès."
- restore_failed_alert: "Impossible de restaurer cet utilisateur."
+ restored_notice: Utilisateur restauré avec succès.
+ restore_failed_alert: Impossible de restaurer cet utilisateur.
set_user:
- person_or_user_missing_alert: "Utilisateur non trouvé."
+ person_or_user_missing_alert: Utilisateur non trouvé.
check_deletion_permissions:
- higher_privileges: "Impossible de supprimer un utilisateur avec des privilèges égaux ou supérieurs."
+ higher_privileges: Impossible de supprimer un utilisateur avec des privilèges égaux ou supérieurs.
require_super_admin:
- restore_denied_alert: "Seul le super-admin peut restaurer des utilisateurs."
+ restore_denied_alert: Seul le super-admin peut restaurer des utilisateurs.
payments:
create:
- success: "Paiement créé avec succès"
+ success: Paiement créé avec succès
+ failure_alert: 'Erreur lors de la création du paiement: %{message}'
update:
- success: "Paiement mis à jour avec succès"
+ success: Paiement mis à jour avec succès
+ failure_alert: 'Erreur lors de la mise à jour: %{message}'
destroy:
- destroyed: "Paiement supprimé avec succès"
+ destroyed: Paiement supprimé avec succès
+ failure_alert: 'Erreur lors de la suppression: %{message}'
process_payment:
- processed: "Paiement traité avec succès"
- already_processed: "Paiement déjà traité"
-
+ processed: Paiement traité avec succès
+ already_processed: Paiement déjà traité
+ failure_alert: 'Erreur lors du traitement: %{message}'
checkout:
success:
- payment_ok: "Paiement réussi et commande mise à jour."
- payment_failed_alert: "Paiement non réussi, commande non mise à jour."
+ payment_ok: Paiement réussi et commande mise à jour.
+ payment_failed_alert: Paiement non réussi, commande non mise à jour.
cancel:
- cancelled_alert: "Le paiement a été annulé."
-
+ cancelled_alert: Le paiement a été annulé.
contacts:
create:
- blank_fields: "Veuillez remplir tous les champs du formulaire."
- sent_notice: "Votre message a été envoyé avec succès ! Nous revenons vers vous rapidement."
- send_error: "Une erreur est survenue lors de l'envoi. Réessaie dans quelques instants."
-
+ blank_fields: Veuillez remplir tous les champs du formulaire.
+ sent_notice: Votre message a été envoyé avec succès ! Nous revenons vers vous rapidement.
+ send_error: Une erreur est survenue lors de l'envoi. Réessaie dans quelques instants.
event_interests:
create:
- profile_incomplete: "Votre profil n'est pas complet. Veuillez contacter l'administration."
- interest_added: "Vous êtes maintenant intéressé par cet événement !"
- interest_error: "Erreur lors de l'ajout de votre intérêt"
+ profile_incomplete: Votre profil n'est pas complet. Veuillez contacter l'administration.
+ interest_added: Vous êtes maintenant intéressé par cet événement !
+ interest_error: Erreur lors de l'ajout de votre intérêt
destroy:
- interest_removed: "Vous n'êtes plus intéressé par cet événement"
- interest_remove_error: "Erreur lors de la suppression de votre intérêt"
-
+ interest_removed: Vous n'êtes plus intéressé par cet événement
+ interest_remove_error: Erreur lors de la suppression de votre intérêt
passwords:
create:
- reset_instructions_sent_generic: "Instructions de réinitialisation du mot de passe envoyées (si un utilisateur avec cette adresse e-mail existe)."
+ reset_instructions_sent_generic: Instructions de réinitialisation du mot de passe envoyées (si un utilisateur avec cette adresse e-mail existe).
update:
- password_reset_success: "Le mot de passe a été réinitialisé."
+ password_reset_success: Le mot de passe a été réinitialisé.
request_reset:
- sent_to_email_notice: "Instructions de réinitialisation du mot de passe envoyées à %{email}."
- must_be_signed_in_alert: "Vous devez être connecté pour effectuer cette action."
- invalid_token_alert: "Le lien de réinitialisation du mot de passe est invalide ou a expiré."
-
+ sent_to_email_notice: Instructions de réinitialisation du mot de passe envoyées à %{email}.
+ must_be_signed_in_alert: Vous devez être connecté pour effectuer cette action.
+ invalid_token_alert: Le lien de réinitialisation du mot de passe est invalide ou a expiré.
registrations:
create:
- success_notice: "Inscription réussie !"
-
+ success_notice: Inscription réussie !
sessions:
- rate_limited_alert: "Rééssayez plus tard"
+ rate_limited_alert: Rééssayez plus tard
create:
- login_success_notice: "Connexion réussie !"
- invalid_credentials_flash: "Email ou mot de passe invalide"
- invalid_credentials_redirect: "Email ou mot de passe invalide"
+ login_success_notice: Connexion réussie !
+ invalid_credentials_flash: Email ou mot de passe invalide
+ invalid_credentials_redirect: Email ou mot de passe invalide
destroy:
- signed_out_notice: "Déconnecté avec succès !"
-
+ signed_out_notice: Déconnecté avec succès !
settings:
update:
- saved_notice: "Vos modifications ont été enregistrées avec succès"
-
+ saved_notice: Vos modifications ont été enregistrées avec succès
users:
update:
- profile_updated: "Votre profil a été mis à jour avec succès."
+ profile_updated: Votre profil a été mis à jour avec succès.
destroy:
- deleted_notice: "Votre compte a été supprimé avec succès."
- destroy_failed_alert: "Impossible de supprimer votre compte. Veuillez contacter l'assistance."
+ deleted_notice: Votre compte a été supprimé avec succès.
+ destroy_failed_alert: Impossible de supprimer votre compte. Veuillez contacter l'assistance.
change_newsletter_status:
- redirect_manage_in_settings_alert: "Gérez votre newsletter depuis vos paramètres."
+ redirect_manage_in_settings_alert: Gérez votre newsletter depuis vos paramètres.
newsletter_signup:
- honeypot_thanks_notice: "Merci pour votre inscription!"
- email_blank_alert: "Veuillez entrer une adresse email valide."
- manage_newsletter_notice: "Gérez votre newsletter depuis vos paramètres en cochant/décochant la case."
+ honeypot_thanks_notice: Merci pour votre inscription!
+ email_blank_alert: Veuillez entrer une adresse email valide.
+ manage_newsletter_notice: Gérez votre newsletter depuis vos paramètres en cochant/décochant la case.
unsubscribe_by_token:
- invalid_token_alert: "Token de désinscription invalide."
-
+ invalid_token_alert: Token de désinscription invalide.
payment_line:
item_description:
- membership_fallback: "Adhésion"
- membership_type_fallback: "Type d'adhésion"
- contribution_fallback: "Cotisation"
- donation_fallback: "Donation"
+ membership_fallback: Adhésion
+ membership_type_fallback: Type d'adhésion
+ contribution_fallback: Cotisation
+ donation_fallback: Donation
descriptions:
- membership: "Adhésion %{name}"
+ membership: Adhésion %{name}
contribution_formula: "%{name} (%{duration})"
- membership_type: "Adhésion %{name}"
-
+ membership_type: Adhésion %{name}
services:
web:
user_registration:
- accept_cgu: "Vous devez accepter les CGU pour continuer."
- accept_privacy: "Vous devez accepter la politique de confidentialité pour continuer."
-
+ accept_cgu: Vous devez accepter les CGU pour continuer.
+ accept_privacy: Vous devez accepter la politique de confidentialité pour continuer.
+ validation:
+ invalid_data: Données invalides
+ invalid_data_with_details: 'Données invalides : %{details}'
mailers:
account_claim_mailer:
confirmation_email:
- subject: "Confirmez votre revendication de compte - Le Circographe"
-
+ subject: Confirmez votre revendication de compte - Le Circographe
passwords_mailer:
reset:
- subject: "Réinitialisez votre mot de passe"
-
+ subject: Réinitialisez votre mot de passe
user_mailer:
welcome_by_admin:
- subject: "Bienvenue au Circographe ! "
+ subject: 'Bienvenue au Circographe ! '
welcome_email:
- subject: "Bienvenue au Circographe ! "
+ subject: 'Bienvenue au Circographe ! '
membership_expiration_reminder:
- subject: "Votre adhésion arrive à expiration !"
+ subject: Votre adhésion arrive à expiration !
+ breadcrumbs:
+ admin:
+ common:
+ administration: Administration
+ dashboard: Tableau de bord
+ edit: Modifier
+ users:
+ members_list: Liste d'adhérents
+ create_web_account: Créer un compte web
+ new_member: Nouvel adhérent
+ memberships:
+ management: Gestion des Adhésions
+ membership: Adhésion
+ upgrade_to_circus: Upgrade vers Cirque
+ new_membership: Nouvelle adhésion
+ edit_membership: Modifier adhésion
+ payments:
+ history: Historique des paiements
+ management: Gestion des paiements
+ subscription_plans:
+ plans: Plans de cotisation
+ plan_named: 'Plan : %{name}'
+ new_contribution: Nouvelle cotisation
+ edit_named: 'Modifier : %{name}'
+ opening_hours:
+ title: Horaires d'ouverture
+ events:
+ events: Événements
+ new_event: Nouvel événement
+ attendances:
+ management: Gestion des présences
+ attendance_number: 'Présence #%{id}'
+ new_attendance: Nouvelle présence
+ membership_types:
+ types: Types d'Adhésion
+ type_named: 'Type : %{name}'
+ new_type: Nouveau type d'adhésion
+ edit_named: 'Modifier : %{name}'
+ attendance_lists:
+ lists: Listes de présence
+ new_list: Nouvelle liste
+ health_reports:
+ integrity_report: Rapport d'intégrité
+ helpers:
+ admin:
+ users:
+ display:
+ not_provided: Non renseigné
+ no_email: Pas d'email
+ name_not_provided: Nom non renseigné
+ layouts:
+ application:
+ title: Le Circographe
+ views:
+ shared:
+ flash:
+ close: Fermer
+ navbar:
+ open_menu: Ouvrir le menu
+ place: Le Lieu
+ circus: Le Cirque
+ graphic_arts: Les Arts Graphiques
+ how_it_works: Fonctionnement
+ activities: Nos Activités
+ join: Adhérer
+ about: À Propos
+ contact: Nous contacter
+ faq: F.A.Q
+ my_space: Mon Espace
+ user: Utilisateur
+ profile: Profil
+ dashboard: Tableau de bord
+ settings: Paramètres
+ logout: Déconnexion
+ sign_in: Se connecter
+ sign_up: S'inscrire
+ association: L'Association
+ news: Actualités
+ our_news: Nos actualités
+ blog_newsletters: Blog & Newsletters
+ photo_gallery: Galerie Photos
+ events: Nos Événements
+ footer:
+ newsletter_cta: Restez informé en souscrivant à notre newsletter
+ email_placeholder: Votre Adresse Email
+ sign_up: S'inscrire
+ discover: Découvrir
+ about: À propos
+ place: Le Lieu
+ activities: Nos activités
+ participate: Participer
+ join: Adhérer
+ contact: Nous contacter
+ map: Plan d’accès
+ resources: Ressources
+ faq: F.A.Q
+ terms: Mentions légales
+ privacy: Politique de confidentialité
+ brand: Le Circographe
+ all_rights_reserved: Tous Droits Réservés.
+ admin:
+ users:
+ new:
+ page_title: Créer un nouvel utilisateur
+ page_subtitle: Ajoutez un nouvel utilisateur au système
+ user_information: Informations de l'utilisateur
+ create_web_account: Créer un compte web
+ create_web_account_help: Permettre à cette personne de se connecter et gérer son profil en ligne
+ system_role: Rôle système
+ roles:
+ web_visitor: Visiteur web
+ volunteer: Bénévole
+ admin: Administrateur
+ temporary_password_notice: Un mot de passe temporaire sera généré et envoyé par email
+ index:
+ page_title: Gestion des Adhérents
+ page_subtitle: Interface unifiée pour gérer les personnes, adhésions et cotisations
+ statistics: Statistiques
+ total_people: Total personnes
+ new_yesterday: Nouveaux (hier)
+ basic_memberships: Adhésions Basic
+ circus_memberships: Adhésions Circus
+ active_memberships: Adhésions actives
+ without_account: Sans compte
+ create_member: Créer un Adhérent
+ pages:
+ about:
+ page_title: Le Circographe - À propos
+ join_community: Rejoindre la communauté
+ heading: À propos du Circographe
+ hero_description: Laboratoire de pratiques artistiques partagées où cirque et arts graphiques se rencontrent — autogestion et solidarité.
+ join: Nous rejoindre
+ discover_place: Découvrir le lieu
+ our_mission: Notre mission
+ mission_heading: Un espace de création partagé et accessible
+ mission_text_1: Offrir un espace de pratique, de création et de transmission accessible à toutes et tous.
+ mission_text_2: 'Gouvernance autogérée : les membres s’engagent bénévolement, partagent les clés et co-programment la vie du lieu.'
diff --git a/db/migrate/20260430204500_backfill_user_person_and_enforce_not_null.rb b/db/migrate/20260430204500_backfill_user_person_and_enforce_not_null.rb
new file mode 100644
index 00000000..77f10e28
--- /dev/null
+++ b/db/migrate/20260430204500_backfill_user_person_and_enforce_not_null.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class BackfillUserPersonAndEnforceNotNull < ActiveRecord::Migration[8.1]
+ class MigrationUser < ApplicationRecord
+ self.table_name = "users"
+ end
+
+ class MigrationPerson < ApplicationRecord
+ self.table_name = "people"
+ end
+
+ def up
+ MigrationUser.where(person_id: nil).find_each do |user|
+ person = MigrationPerson.create!(
+ first_name: "Web",
+ last_name: "User",
+ email: user.email_address
+ )
+ user.update_columns(person_id: person.id)
+ end
+
+ change_column_null :users, :person_id, false
+ end
+
+ def down
+ change_column_null :users, :person_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ec4bd163..968cadbf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_04_27_092706) do
+ActiveRecord::Schema[8.1].define(version: 2026_04_30_204500) do
create_table "account_claims", force: :cascade do |t|
t.string "confirmation_token", null: false
t.datetime "created_at", null: false
@@ -360,7 +360,7 @@
t.datetime "password_reset_sent_at"
t.string "password_reset_token"
t.string "password_salt"
- t.bigint "person_id"
+ t.bigint "person_id", null: false
t.integer "system_role", default: 3, null: false
t.datetime "updated_at", null: false
t.index ["deleted"], name: "index_users_on_deleted"
diff --git a/docs/README.md b/docs/README.md
index cc70cd58..3d8883ae 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur, équipe
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : structure réelle du dossier `docs/`.
Index de la documentation Markdown du projet. Pour le démarrage et le déploiement, voir le [`README.md`](../README.md) à la racine.
@@ -77,7 +77,7 @@ Chaque document Markdown déclare un statut dans son header :
## Domaine et vocabulaire
- [`glossary.md`](glossary.md) — lexique canonique FR/EN, termes interdits.
-- [`domain_model.md`](domain_model.md) — diagramme Mermaid + responsabilités des agrégats.
+- [`domain_model.md`](domain_model.md) — diagramme Mermaid + responsabilités des agrégats (invariant `User` → `Person`).
- [`payments.md`](payments.md) — `Payment`, `PaymentLine`, `Donation` et la dette legacy `item_type:"Payment"`.
- [`domain/business_logic.md`](domain/business_logic.md) — règles métier complètes (adhésion, cotisation, paiements).
diff --git a/docs/architecture/models.md b/docs/architecture/models.md
index 0b1ad4f0..9ebf0632 100644
--- a/docs/architecture/models.md
+++ b/docs/architecture/models.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/`, `app/models/concerns/`, `db/schema.rb`, `spec/models/`.
> **Vocabulaire DDD-light** (voir [`../glossary.md`](../glossary.md))
@@ -20,7 +20,7 @@ Ce document remplace l'ancien trio `docs/MODEL_EVALUATION.md` + `docs/CONCERNS_A
```
Person (CRM, données personnelles)
- ├─> User (authentification, optionnel)
+ ├─> User (authentification — au plus un ; tout User a une Person)
├─> Membership (adhésion annuelle)
├─> Payment (transactions)
├─> BookOfEntry (cible : Contribution — cotisation cirque)
@@ -33,9 +33,10 @@ Les formules de cotisation sont stockées dans `SubscriptionPlan` *(cible : `Con
### Points forts confirmés
- Séparation claire **auth** (`User`) vs **profil** (`Person`).
+- **Invariant** : `User` → `Person` obligatoire (`person_id` NOT NULL) ; `Person` peut exister sans `User`.
- One Source of Truth pour les données personnelles.
- Délégation propre `User → Person`.
-- Soft-delete `Person` sans perdre `User`.
+- Côté `Person`, `has_one :user, dependent: :restrict_with_error` : pas de suppression incompatible tant qu’un compte web existe (archive / RGPD).
- `Payment` → `PaymentLine` polymorphique = un paiement peut regrouper adhésion + cotisation + don.
- Audit trail complet via `PaymentAuditLog` + UUID externe.
- Versioning sur `MembershipType` et `SubscriptionPlan` *(cible : `ContributionFormula`)* (`version`, `effective_from/until`, `change_reason`, `created_by_user_id`).
@@ -136,7 +137,7 @@ Cadre de **stabilité / risque** pour prioriser les tests. Pour les priorités a
#### Zone 2 — fonctionnels mais à stabiliser
-`Web::UserRegistration`, `People::Register`, `People::PaymentUpdater`, `People::PaymentCanceller`, `People::PaymentRestorer`, `People::AccountLinker`, `UserManagement::UserDeleter`, `People::AccountMerger`.
+`Web::UserRegistration`, `People::Register`, `People::PaymentUpdater`, `People::PaymentCanceller`, `People::PaymentRestorer`, `People::AttachUserToPerson`, `People::AccountLinker`, `UserManagement::UserDeleter`, `People::AccountMerger`.
> Les classes `EventManagement::EventCreator/Updater/Deleter` existent dans `app/services/event_management/` mais sont **orphelines** : aucun contrôleur ne les appelle (CRUD inline dans `Admin::EventsController`). Cleanup tracé dans [`../internal/todo.md`](../internal/todo.md).
diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md
index 13429780..3e29cd96 100644
--- a/docs/architecture/overview.md
+++ b/docs/architecture/overview.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/person.rb`, `app/models/user.rb`, `app/components/`, `app/services/people/`.
> **Vocabulaire** : le composant a été renommé `contribution_status_badge_component` (cible). Les services `People::Subscription*` restent à renommer en `People::Contribution*` lors de la migration DB. Voir [`../glossary.md`](../glossary.md).
@@ -24,12 +24,12 @@ Ce document consolide les bonnes pratiques et l'architecture mise en place lors
## 👤 Person / User - Règles de Cycle de Vie
- **Person = source de vérité** pour l'identité et la finance.
-- **User = compte web** (authentification + permissions), optionnel.
+- **User = compte web** (authentification + permissions). **Données : chaque User a une Person** (`person_id` NOT NULL) ; le compte web reste **optionnel au niveau métier** pour une Person donnée (CRM sans login).
- **Cas supportés** :
- Person sans User (inscription IRL d'abord).
- - User sans Person (inscription web d'abord).
-- **Lien explicite uniquement** : le lien User ↔ Person se fait via un service dédié.
-- **Pas de reliaison implicite** si une Person a déjà un User lié.
+ - Inscription web : création d’un **couple User + Person** (personne minimale puis enrichissement / rattachement à une fiche existante).
+- **Lien / rattachement explicite** : `People::AttachUserToPerson` (nominal), `People::AccountLinker` (orchestration), jamais d’assign direct dans un controller.
+- **Pas de reliaison implicite** si une Person a déjà un User lié (garde-fous dans `AttachUserToPerson`).
- **Pas d'orphelins financiers** : paiements et adhésions restent rattachés à la Person.
---
@@ -37,7 +37,7 @@ Ce document consolide les bonnes pratiques et l'architecture mise en place lors
## 🧭 Service Entry Points (Flux Unifiés)
- **Création Person / User / Membership** : `People::Register`
-- **Lien User ↔ Person** : `People::AccountLinker`
+- **Rattachement User ↔ Person** : `People::AttachUserToPerson` ; **orchestration** : `People::AccountLinker`
- **Achat adhésion** : `People::MembershipCreator`
- **Achat cotisation** : `People::SubscriptionCreator` *(cible : `People::ContributionCreator`)*
- **Mise à jour User + Person** : `UserManagement::UserUpdater`
diff --git a/docs/architecture/services.md b/docs/architecture/services.md
index 1097c7f3..f5516f71 100644
--- a/docs/architecture/services.md
+++ b/docs/architecture/services.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/services/people/`, `app/services/event_management/`, `app/services/attendance_management/`, `app/services/user_management/`.
**Création initiale:** 2025-01-31
@@ -23,17 +23,18 @@ Les services suivent le pattern **Service Object avec ActiveModel::Model** :
### Person (Entity) vs User (Account)
- **Person = Entity CRM** : fiche métier unique qui contient l'identité, l'historique financier (adhésions, cotisations, paiements) et tous les attributs d’usage.
-- **User = Account** : accès web optionnel (email, mot de passe, rôle) qui délègue tous ses attributs de profil à `Person` via `delegate`.
+- **User = Account** : accès web (email, mot de passe, rôle) qui délègue tous ses attributs de profil à `Person` via `delegate`. **Données : tout `User` a une `Person`** (`belongs_to :person`, NOT NULL) ; à la création web sans fiche existante, une `Person` minimale est créée par callback sur `User`.
- **Règles clés** :
- Créer/éditer la fiche métier via `People::Register` / `People::PersonCreator` ; le compte web est créé via `People::UserAccountCreator` si besoin.
- - Supprimer un `User` ne détruit pas la `Person` (relation `has_one :user, dependent: :nullify`).
+ - Rattacher un compte existant à une fiche CRM : **`People::AttachUserToPerson`** (nominal) ; **`People::AccountLinker`** encapsule attach + nettoyage éventuel (`People::AccountMerger`).
+ - Supprimer un `User` ne détruit pas la `Person`. Côté `Person`, `has_one :user, dependent: :restrict_with_error` empêche une suppression de fiche incompatible tant qu’un `User` existe — passer par archive / RGPD.
- Supprimer une `Person` passe par `SoftDeletable` (`Person#archive!`) avec garde-fous financiers (`has_financial_data?`).
- Toutes les opérations financières (`People::Payment*`, `People::Subscription*` *(cible : `People::Contribution*`)*, `People::Register`) travaillent **exclusivement** sur `Person`.
Cette séparation “Entity / Account” garantit :
- pas de perte d’historique quand un utilisateur supprime son compte web,
- la possibilité de gérer des personnes sans compte web (inscriptions papier, mineurs, bénévoles),
-- une liaison safe quand un compte web est créé après coup ou par l’admin.
+- une liaison explicite et auditée (`AttachUserToPerson` / `AccountLinker`) quand un compte web doit être relié à une fiche existante.
## Organisation par Domaine
@@ -66,17 +67,18 @@ Cette séparation “Entity / Account” garantit :
- `Admin::Users::PaymentsController` (create, update, destroy via `People::PaymentCreator` multi-lignes)
**Donations — état actuel et cible**
-- **Cible** : une donation est une `PaymentLine` avec `item_type: "Donation"`. Aucune `PaymentLine` ne doit avoir `item_type: "Payment"`.
-- **Code actuel** : `People::PaymentCreator` réécrit silencieusement les lignes de don en `item_type: "Payment"` et `item_id: payment.id` (cf. `app/services/people/payment_creator.rb` L92). Cette dette technique est tracée dans `phase1-donation-fix` (voir [`../payments.md`](../payments.md)).
-- **Comportement attendu côté appelant** : passer `item_type: "Donation"` et `item_id: payment.id` (ou `person.id` selon le flow). Le service fait le reste — y compris la réécriture legacy temporaire.
+- **Cible** : une donation est une `PaymentLine` avec `item_type: "Donation"` et `item_id` adapté (souvent `payment.id` pour les dons « libres »). Aucune nouvelle ligne ne doit utiliser `item_type: "Payment"` pour un don.
+- **Code actuel** : `People::PaymentCreator` garde `item_type: "Donation"` sur la ligne simple lorsque le flux est un don (`donation_line?`). Les anciennes lignes en base peuvent encore être `item_type: "Payment"` — backfill / reporting : voir [`../payments.md`](../payments.md) et `phase1-donation-fix`.
+- **Comportement attendu côté appelant** : passer `item_type: "Donation"` (ou laisser le défaut du service) ; fournir montants cohérents avec `total_cents`.
- **Validation** : la somme des `payment_lines` doit égaler `total_cents` ; sinon, `failure`.
-### ✅ People::AccountLinker (Support CRM)
-- `People::AccountLinker` relie un compte web existant à une fiche CRM (`people.account_linked`)
-- Utilisé par scripts de maintenance (`scripts/fix_person_user_merge.rb`)
+### ✅ People::AttachUserToPerson & People::AccountLinker (liaison CRM)
+- **`People::AttachUserToPerson`** : rattache un `User` à une `Person` cible (refuse si la cible a déjà un autre `User`), instrumentation `people.user_attached`.
+- **`People::AccountLinker`** : orchestration (attach via `AttachUserToPerson`, merge optionnel de l’ancienne fiche avec `People::AccountMerger`). Événement `people.account_linked`.
+- Utilisé aussi par `AccountClaimManagement::AccountClaimConfirmer` (attach direct possible) et scripts (`scripts/fix_person_user_merge.rb`).
### ⚠️ PersonManagement (Legacy ciblé)
-- Ancien namespace conservé uniquement pour compatibilité. Les nouvelles fusions doivent passer par `People::AccountLinker` ou `People::AccountMerger`.
+- Ancien namespace conservé uniquement pour compatibilité. Les nouvelles fusions / liaisons doivent passer par `People::AttachUserToPerson`, `People::AccountLinker` ou `People::AccountMerger`.
### ✅ UserManagement (Stable)
- `UserDeleter` - Suppression d'utilisateurs (Person)
diff --git a/docs/design/color_system.md b/docs/design/color_system.md
index bb2c4b17..9050203b 100644
--- a/docs/design/color_system.md
+++ b/docs/design/color_system.md
@@ -186,7 +186,7 @@ Exemple :
2. Appliquer les étapes ci-dessus progressivement (commit par step).
3. Tester en local :
- `bin/dev` + vérifier pages `home`, `faq`, `about`, `news`, formulaires.
- - `bin/rails test` pour s’assurer qu’aucune régression JavaScript.
+ - `bundle exec rspec` pour s’assurer qu’aucune régression applicative.
4. Ouvrir une PR pour revue, intégrer feedback, merger une fois validé.
## 10. Bonnes Pratiques
diff --git a/docs/development/testing.md b/docs/development/testing.md
index c8fdbd65..dcd9a4b9 100644
--- a/docs/development/testing.md
+++ b/docs/development/testing.md
@@ -310,10 +310,10 @@ bundle exec rspec
bundle exec rubocop app --only Rails/HelperInstanceVariable --force-exclusion
```
-- **Prévoir l’auto-correction sans écrire** :
+- **Prévisualiser un lot de cops sans écrire** :
```bash
- bundle exec rubocop -A --dry-run --force-exclusion
+ bundle exec rubocop --only NomDuCop chemins... --force-exclusion
```
Puis corriger avec `bundle exec rubocop -a` (**safe**) ou `-A` (plus agressif — à utiliser avec prudence).
@@ -436,10 +436,14 @@ L'ensemble des services `People::*`, `AccountClaimManagement::*`, `AttendanceMan
### RuboCop — rollout progressif (Phase 2+)
-- **Baseline** : `rubocop-rails-omakase` dans [`.rubocop.yml`](../../.rubocop.yml) comme **seule** base héritée ; exclusions projet documentées (permanent / temporaire) + override Ezam `Layout/EndOfLine: lf`. Le dossier `test/**/*` est **linté** avec le reste du Ruby applicatif (`spec/` l’était déjà).
+- **Baseline** : `rubocop-rails-omakase` dans [`.rubocop.yml`](../../.rubocop.yml) comme **seule** base héritée ; exclusions projet documentées (permanent / temporaire) + override Ezam `Layout/EndOfLine: lf`. Le projet est en mode **RSpec-only** et le dossier legacy `test/` est retire.
-- **Lot B2 (élargissement)** — périmètre + conventions Rails cumulées : `test/**/*` inclus ; cops `Rails/` activés localement avec prudence (Omakase laisse la plupart du département `Rails` désactivé) — actuellement : `Rails/HttpStatus`, `Rails/Output`, `Rails/Delegate`, `Rails/StrongParametersExpect`, `Rails/PluralizationGrammar`, `Rails/UniqBeforePluck`, `Rails/HelperInstanceVariable`, `Rails/I18nLocaleTexts` (messages utilisateur en `t()` / YAML, pas de chaînes littérales dans contrôleurs, mailers et modèles). Étendre la liste uniquement via petites PR après `bundle exec rubocop` / `bundle exec rspec` verts.
+- **Lot B2 (historique)** — le rollout `Rails/*` a ete fait par petits lots. Aujourd'hui, conserver la meme approche: petites PR, puis `bundle exec rubocop` + `bundle exec rspec` verts.
- **Specs et locale** : `config.i18n.default_locale` est `:fr` ; les messages ActiveRecord / `number_to_currency` suivent `rails-i18n` (fr). Dans les tests de validations, préférer `I18n.t('errors.messages.blank')`, `I18n.t('errors.messages.required')`, etc., plutôt que du texte anglais codé en dur ; pour les flashes ou sujets de mail, utiliser `I18n.t('…')` avec la même clé que l’application.
+- **Parite FR/EN** : `config/locales/en.yml` est maintenu en parite de cles avec `config/locales/fr.yml`. Utiliser `bundle exec rake i18n:check_keys` avant merge.
+- **Auth** : la stack d'authentification reste le systeme natif Rails 8 ; ne pas introduire Devise.
+- **Fallback transitoire** : `config.i18n.fallbacks = { en: %i[fr] }` est volontairement temporaire pendant la complétion de `config/locales/en.yml`. Objectif long terme : parité `fr/en` sans fallback implicite pour l’UX anglaise.
+- **Parité des clés locales** : utiliser `bundle exec rake i18n:check_keys` pour lister les clés manquantes entre `fr.yml` et `en.yml` et prioriser les traductions manquantes.
- **Jobs** : [`.github/workflows/ci-lint-audit.yml`](../../.github/workflows/ci-lint-audit.yml) (`lint`) et [`.github/workflows/ci-auto-lint.yml`](../../.github/workflows/ci-auto-lint.yml) — RuboCop est **bloquant** lorsque la baseline est verte (`bundle exec rubocop --format github --force-exclusion`).
- **Élargissement** : activer les cops **par petits lots**, une PR par lot ; pas de refactors métier ni renommage de vocabulaire domaine sans accord ([glossaire](../glossary.md)).
- **Lots suivants suggérés** : après vérif des offenses — LOW (`Style/TrailingCommaInArrayLiteral` / `HashLiteral` si pertinent), puis MEDIUM (`Performance/*`, `Rails/*` au cas par cas), puis HIGH (`Lint/*` sur flux, `Metrics/*`, fichiers `app/models` / `app/services` sensibles) en revue manuelle uniquement.
@@ -490,3 +494,11 @@ L'ensemble des services `People::*`, `AccountClaimManagement::*`, `AttendanceMan
- [`../architecture/services.md`](../architecture/services.md) — catalogue de services.
- [`../architecture/controllers.md`](../architecture/controllers.md) — état des contrôleurs.
- [`../architecture/models.md`](../architecture/models.md) — modèles, concerns, zones de stabilité.
+
+## 11. Audit `Rails/SkipsModelValidations` (classification)
+
+- `risky_bypass` (priorité traitée): `app/models/user.rb` (anonymisation), suivi ciblé `app/models/payment.rb`.
+- `intentional_batch` (gardé sous transaction + commentaire explicite): `app/services/people/account_merger.rb`, `app/models/concerns/versionable.rb`, `app/models/concerns/duplicatable.rb`.
+- `legacy/archive` (exclusion ciblée possible): `docs/rake_archive/migrate_to_person_architecture.rake`.
+- `consolidated_path`: la fusion applicative doit passer par `People::AccountMerger` (éviter les duplications de logique dans d'autres services).
+- `identity_invariant`: en cible projet, un `User` est toujours rattaché à une `Person` (phase de verrouillage en cours).
diff --git a/docs/domain/business_logic.md b/docs/domain/business_logic.md
index ab0731f9..2fde1cfd 100644
--- a/docs/domain/business_logic.md
+++ b/docs/domain/business_logic.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur, métier
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/person.rb`, `app/models/membership.rb`, `app/services/people/*.rb`, `db/seeds/membership_types.rb`, `db/seeds/subscription_plans.rb`.
**Application:** Gestion complète pour association de cirque
@@ -181,12 +181,13 @@ enum system_role: [:super_admin, :admin, :volunteer, :web_visitor]
#### Person Architecture (Nouvelle)
- **Entity / Account pattern:**
- **Person = Entity CRM** (identité unique, historique financier, soft delete via `SoftDeletable`).
- - **User = Account** (accès web optionnel) qui référence une `Person` existante (`belongs_to :person`).
+ - **User = Account** (accès web) **toujours lié à une `Person`** (`belongs_to :person`, NOT NULL). Une Person peut exister sans User ; l’inverse non.
- **Conséquences :**
- - Création front : on `find_or_create_by` Person avant de créer User.
+ - Création web : `People::Register` / `Web::UserRegistration` ou équivalent ; sinon callback sur `User` crée une **Person minimale** si absente.
- Création admin : `People::Register` orchestre Person + User (+ Membership optionnel) ; `People::PersonCreator` disponible pour les scripts.
+ - Rattachement / enrichissement : `People::AttachUserToPerson`, `People::AccountLinker`, fusions `People::AccountMerger`.
- Suppression User : coupe l’accès web (`destroy`), la Person et ses paiements restent.
- - Suppression Person : passe par `UserManagement::UserDeleter` qui archive la Person (`Person#archive!`) seulement si aucune donnée financière (sauf super_admin).
+ - Suppression Person : passe par `UserManagement::UserDeleter` qui archive la Person (`Person#archive!`) seulement si aucune donnée financière (sauf super_admin). Tant qu’un `User` existe, `Person` ne peut pas être détruite implicitement (`restrict_with_error`).
- **Délégation:** User délègue attributs à Person (`delegate :full_name, :phone, ...`).
#### Tarifs Réduits
@@ -641,7 +642,7 @@ Admin::UserCreationForm
| `PersonManagement::PersonCreator` (backend, web) | `People::PersonCreator` | ❌ Supprimé |
| `UserManagement::AccountCreator` | `People::UserAccountCreator` | ❌ Supprimé |
| `UserManagement::UserCreator` | `People::UserAccountCreator` | ✅ Branché |
-| `People::AccountLinker` (script merge) | `People::AccountLinker` | ✅ Branché |
+| `People::AccountLinker` (script merge) | `People::AccountLinker` (+ `People::AttachUserToPerson`) | ✅ Branché |
| `MembershipManagement::MembershipCreator` | `People::MembershipCreator` | ❌ Supprimé |
| `MembershipManagement::MembershipUpgrader` | `People::MembershipUpgrader` | ❌ Supprimé |
| `MembershipManagement::MembershipUpdater` | `People::MembershipUpdater` | ❌ Supprimé |
@@ -689,8 +690,8 @@ Admin::UserCreationForm
## Architecture Actuelle
-**Person-Based:** User → Person (relation 1-1)
-**Résultat:** Séparation données authentification vs profil
+**Person-Based:** `User` `belongs_to` `Person` (obligatoire) ; `Person` `has_one` `User` (optionnel). Relation 1‑to‑0..1 du point de vue Person.
+**Résultat:** Séparation données authentification vs profil, sans « User sans Person » en base.
## Concerns Utilisés
diff --git a/docs/domain_model.md b/docs/domain_model.md
index cd295dae..2da81c59 100644
--- a/docs/domain_model.md
+++ b/docs/domain_model.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `db/schema.rb`, `app/models/person.rb`, `app/models/membership.rb`, `app/models/payment.rb`, `app/models/payment_line.rb`.
> Vocabulaire utilisé : voir [glossary.md](glossary.md). Quand le code n'est pas encore aligné sur le vocabulaire cible, l'alias legacy est indiqué entre parenthèses.
@@ -14,7 +14,7 @@
```mermaid
erDiagram
- Person ||--o| User : "compte web (optionnel)"
+ Person ||--o| User : "≤1 compte web par Person"
Person ||--o{ Membership : "souscrit"
Person ||--o{ Contribution : "achète"
Person ||--o{ Attendance : "présence"
@@ -40,7 +40,7 @@ erDiagram
> **Légende** :
> - `Contribution` : code actuel `BookOfEntry` (rename planifié `phase3-model-rename`).
> - `ContributionFormula` : code actuel `SubscriptionPlan` (rename planifié `phase3-model-rename`).
-> - `Donation` : pas encore matérialisé en modèle distinct ; dette legacy `item_type: "Payment"` à éliminer (voir [payments.md](payments.md)).
+> - `Donation` : pas encore un modèle ActiveRecord dédié ; lignes `PaymentLine` en `"Donation"` à la création ; legacy DB `item_type: "Payment"` à éliminer (voir [payments.md](payments.md)).
---
@@ -49,6 +49,7 @@ erDiagram
### 2.1 Personnes et comptes
#### `Person` — Aggregate Root CRM
+- **Compte web** : `has_one :user, dependent: :restrict_with_error` — au plus un `User` ; pas de suppression « dure » de la fiche tant qu'un compte web existe (flux RGPD / archive à utiliser).
- **Identité** : `full_name`, `phone`, `email`, `address`, `birth_date`.
- **Tarif réduit** : `reduced_rate_eligible`, `reduced_rate_reason`, `reduced_rate_proof`.
- **Soft delete** : `deleted_at` (concern `SoftDeletable`).
@@ -61,10 +62,12 @@ erDiagram
- `Person#archive!` / `Person#restore!`.
- **Garde-fou** : `has_financial_data?` empêche la suppression dure si l'historique financier est non vide.
-#### `User` — Compte web (optionnel)
+#### `User` — Compte web
+- **Invariant** : `belongs_to :person` **obligatoire** en base (`users.person_id` NOT NULL). À la création sans `person` explicite, le modèle attache une `Person` minimale (prénom/nom stub + email aligné sur le compte).
- **Authentification** : email, password, sessions, password_reset_token.
- **Rôle** : `system_role` enum `:super_admin | :admin | :volunteer | :web_visitor`.
- **Délégation** : `delegate :full_name, :phone, ... to: :person`.
+- **Liaison / fusion** : rattachement nominal `People::AttachUserToPerson` ; orchestration admin/scripts `People::AccountLinker` ; fusion de fiches `People::AccountMerger`.
- **Soft delete** : `User#archive!` (admin uniquement).
---
@@ -117,7 +120,7 @@ erDiagram
- **Invariant** : `payment.payment_lines.sum(:amount_cents) == payment.total_cents`.
#### `Donation` (cible)
-- Pas encore un modèle distinct. Représenté actuellement par une `PaymentLine` avec `item_type: "Payment"` (réécriture par `People::PaymentCreator`). Migration en `phase1-donation-fix` (voir [payments.md](payments.md)).
+- Pas encore un modèle ActiveRecord distinct. Représenté par une `PaymentLine` avec `item_type: "Donation"` à la création (`People::PaymentCreator`). Des lignes historiques peuvent encore avoir `item_type: "Payment"` jusqu’au backfill complet — voir [payments.md](payments.md) et `phase1-donation-fix`.
---
@@ -220,6 +223,8 @@ sequenceDiagram
Reg-->>UI: success(person, user?, membership?)
```
+> **Inscription web** : les flux qui ne passent pas par `People::Register` créent tout de même un couple `User` + `Person` via le callback `User` (personne minimale), puis enrichissement CRM au fil du temps.
+
---
## 5. Documents liés
@@ -228,4 +233,4 @@ sequenceDiagram
- [payments.md](payments.md) — détail Payment / PaymentLine / Donation.
- [migrations/vocabulary_migration.md](migrations/vocabulary_migration.md) — mapping ancien → nouveau.
- [domain/business_logic.md](domain/business_logic.md) — règles métier complètes.
-- [architecture/services.md](architecture/services.md) — services `People::*` et orchestrateurs.
+- [architecture/services.md](architecture/services.md) — services `People::*` et orchestrateurs (`Register`, `AttachUserToPerson`, `AccountLinker`, paiements).
diff --git a/docs/glossary.md b/docs/glossary.md
index 1085e34c..67b5adc6 100644
--- a/docs/glossary.md
+++ b/docs/glossary.md
@@ -2,7 +2,7 @@
> **Statut** : stable (canonique)
> **Public cible** : contributeur, métier
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/*.rb`, `db/schema.rb`, [`migrations/vocabulary_migration.md`](migrations/vocabulary_migration.md).
> **Source de vérité unique** pour le vocabulaire du domaine. Toute nouvelle PR (code, doc, UI, tests) doit utiliser ce vocabulaire. Les termes listés en section « Termes interdits » sont rejetés en revue. Utilisé pendant la migration `phase0` → `phase4`.
@@ -24,8 +24,8 @@
- Confondre `Person` avec `User`.
#### Compte web — `User`
-- **Définition** : compte d'authentification web (email, mot de passe, rôle système). **Optionnel** — relié à une `Person` existante via `belongs_to :person`. Tous les attributs de profil (`full_name`, `phone`…) sont délégués vers `Person`.
-- **Cycle de vie** : créé via `People::UserAccountCreator` (souvent dans `People::Register`). Sa suppression (`User#destroy`) coupe l'accès web mais laisse intact le `Person` et son historique.
+- **Définition** : compte d'authentification web (email, mot de passe, rôle système). **Chaque `User` est obligatoirement relié à une `Person`** (`belongs_to :person`, `person_id` NOT NULL) ; une `Person` peut exister **sans** `User` (adhésion papier, mineurs, etc.). Tous les attributs de profil (`full_name`, `phone`…) sont délégués vers `Person`.
+- **Cycle de vie** : créé via `People::UserAccountCreator` ou callback sur `User` (personne minimale) puis enrichissement CRM ; rattachement explicite via `People::AttachUserToPerson` / `People::AccountLinker`. La suppression du compte (`User#destroy`) coupe l'accès web mais laisse intact le `Person` et son historique (tant que les flux RGPD ne désactivent pas autrement).
- **Usage correct** :
- « Cette personne n'a pas encore de compte web ».
- « Le compte web a été archivé, la fiche personne reste active ».
@@ -104,15 +104,15 @@
- `"MembershipType"` → renouvellement / achat sur le catalogue.
- `"ContributionFormula"` (cible) / `"SubscriptionPlan"` (legacy) → achat d'une cotisation.
- `"Contribution"` (cible) / `"BookOfEntry"` (legacy, rare) → cotisation existante.
- - `"Donation"` → don (cible). **Attention** : actuellement `People::PaymentCreator` réécrit les lignes de don en `item_type: "Payment"` (dette technique tracée — voir [payments.md](payments.md)).
+ - `"Donation"` → don. **Création** : `People::PaymentCreator` conserve `"Donation"` sur les lignes de don. Des lignes **historiques** peuvent encore avoir `item_type: "Payment"` jusqu’au backfill complet — voir [payments.md](payments.md).
- **Invariant** : la somme des lignes = `payment.total_cents`.
- **À éviter** :
- `item_type: "Payment"` pour un don dans toute nouvelle documentation ou code (utiliser `"Donation"`).
#### Don — `Donation`
- **Définition** : paiement volontaire sans contrepartie, conservé pour reçu fiscal éventuel.
-- **Représentation actuelle** : matérialisé par une `PaymentLine` dont l'item est conceptuellement un don, mais stocké techniquement avec `item_type: "Payment"` (legacy à éliminer).
-- **Représentation cible** : `PaymentLine` avec `item_type: "Donation"` et `item_id` stable (id du paiement parent ou modèle dédié — décision en `phase1-donation-fix`).
+- **Représentation actuelle (code)** : `PaymentLine` avec `item_type: "Donation"` (création via `People::PaymentCreator`). Données anciennes : encore `item_type: "Payment"` sur certaines lignes jusqu’à migration — voir `phase1-donation-fix` et [payments.md](payments.md).
+- **Représentation cible** : même schéma polymorphique ; nettoyage DB (`Payment` legacy, champ `payments.donation`, validations strictes sur `item_type`).
- **Usage correct** :
- « Don de 5 € lors d'une adhésion ».
- « Le paiement contient deux lignes : adhésion + don ».
@@ -171,7 +171,7 @@
| Terme français | Terme anglais (code) | Statut | Note |
| --- | --- | --- | --- |
| Personne | `Person` | canonique | source de vérité CRM |
-| Compte web | `User` | canonique | optionnel |
+| Compte web | `User` | canonique | optionnel **pour une Person** (pas toujours de compte) ; **obligatoire pour un User** (toujours une Person liée) |
| Adhésion | `Membership` | canonique | annuel |
| Type d'adhésion | `MembershipType` | canonique | catalogue versionné |
| Cotisation | `Contribution` | **cible** (legacy : `BookOfEntry`) | instance achetée |
diff --git a/docs/internal/todo.md b/docs/internal/todo.md
index 5d02be00..c6ef1480 100644
--- a/docs/internal/todo.md
+++ b/docs/internal/todo.md
@@ -2,9 +2,11 @@
> **Statut** : internal
> **Public cible** : équipe dev
-> **Dernière mise à jour** : 2026-04-27
+> **Dernière mise à jour** : 2026-05-01
> **Provenance** : fusion de l'ancien `to-do.md` (racine) et `docs/TODO.md` (doublon).
>
+> **Identity note (2026-05)** — tout `User` a une `Person` (création minimale si besoin, DB `users.person_id` NOT NULL). Pas de « User sans Person » en données. Voir résumé dans le [README](../../README.md) et la règle [naming-rules.mdc](../../.cursor/rules/naming-rules.mdc).
+>
> **Vocabulary note** — the words `subscription`, `SubscriptionCreator`, `SubscriptionUpgrader`, `SubscriptionPlan`, `BookOfEntry` below refer to the **current code**.
> Target domain vocabulary: `contribution`, `ContributionCreator`, `ContributionUpgrader`, `ContributionFormula`, `Contribution`.
> Single exception: `subscription` is legitimate in the **newsletter** context.
@@ -13,7 +15,7 @@
Ordered from quick wins to long-term work. Each item can be handled incrementally.
## 0) Ground Rules (Architecture + MVC)
-- Document the Person/User lifecycle and ownership rules.
+- Document the Person/User lifecycle and ownership rules. (partial: résumé README + règles Cursor ; manque encore un doc « happy-path » dédié, cf. §3)
- Enforce “Person is source of truth for identity + finance.”
- Ensure controllers remain thin: call services, render/redirect.
- Keep domain logic in models and workflows in services.
@@ -32,14 +34,14 @@ Ordered from quick wins to long-term work. Each item can be handled incrementall
## 2) Medium (flow consistency + integrity)
- Ensure admin registration uses `People::Register` only.
-- Ensure account linking uses `People::AccountLinker` only.
+- Ensure account linking goes through services — **no ad-hoc `user.person = …` in controllers.** Nominal attach: `People::AttachUserToPerson` ; orchestration / compat: `People::AccountLinker` (délègue à `AttachUserToPerson` + merge cleanup si besoin). Les flux « account claim » peuvent appeler `AttachUserToPerson` directement.
- Ensure membership creation uses `People::MembershipCreator` only.
- Ensure subscription purchase uses `People::SubscriptionCreator` only.
- Ensure upgrades use `People::MembershipUpgrader` / `People::SubscriptionUpgrader`.
- Support Person without User (real-life registration first).
-- Support User without Person (web-first signup).
-- Provide explicit admin action to link User ↔ Person.
-- Prevent implicit relinks when a Person already has a User.
+- Web signup: **chaque `User` a une `Person`** (stub minimale à la création) ; enrichissement via édition Person / rattachement / fusion — plus de cas métier « user orphelin sans Person ».
+- Provide explicit admin action to link User ↔ Person. (partial: services prêts ; vérifier couverture UI + permissions)
+- Prevent implicit relinks when a Person already has a User. (partial: `AttachUserToPerson` refuse si la Person cible a déjà un autre User)
- Show offer reason in payment history for offered payments.
- Enforce offer_reason on payments when payment_method == offered (admin edit too).
- Display offer reason in membership/subscription history when offered.
@@ -57,10 +59,10 @@ Ordered from quick wins to long-term work. Each item can be handled incrementall
- Add “Data integrity rules” checklist (no orphans, no overlap, unlimited plan rules).
- Add “Role permissions” doc (who can offer, delete, link, anonymize).
- Maintain `docs/README.md` as the index of truth.
- - Keep `docs/development/testing.md` + `docs/architecture/controllers.md` revalidated against current tests.
+- Keep `docs/development/testing.md` + `docs/architecture/controllers.md` revalidated against current tests (invariant User→Person, RSpec-only, auth native Rails 8 résumés dans README / testing.md).
## 4) Tests (medium -> long)
-- Service specs: Register, AccountLinker, MembershipCreator, SubscriptionCreator.
+- Service specs: Register, AttachUserToPerson, AccountLinker, MembershipCreator, SubscriptionCreator.
- Controller specs for admin flows (success + failure).
- ViewComponent specs for badges/contextual actions.
- Integrity specs for orphan queries.
@@ -73,6 +75,7 @@ Ordered from quick wins to long-term work. Each item can be handled incrementall
- Spec for payment_lines sum == payment total.
## 5) Payments Accountability (longer)
+- Align legacy donation lines: aucune ligne ne doit rester avec `item_type: "Payment"` pour un don (canonique `Donation` ; migration de backfill existante — vérifier jeux de données réels et reporting).
- Remove any user_id references for payments (payments belong to Person). (partially done: User#destroy)
- Require `recorded_by` in all payment flows.
- Use “void/cancel” instead of delete.
@@ -99,7 +102,7 @@ Ordered from quick wins to long-term work. Each item can be handled incrementall
## 8) Rollout Order
1. Health Report panel (visibility first). (done)
2. Fix invalid payment/user linking logic. (done: admin payment/donation links use person_id; removed user_id filter in payments service)
-3. Force registration + linking through services.
+3. Force registration + linking through services. (partial: `People::Register` sur chemins admin/form/web ; rattachement via `AttachUserToPerson` / `AccountLinker` ; auditer le reste des controllers)
4. Replace deletes with anonymization.
5. Clean legacy views.
6. Add/extend tests.
diff --git a/docs/migrations/vocabulary_migration.md b/docs/migrations/vocabulary_migration.md
index aa932378..f884c224 100644
--- a/docs/migrations/vocabulary_migration.md
+++ b/docs/migrations/vocabulary_migration.md
@@ -2,7 +2,7 @@
> **Statut** : stable (transitionnel — disparaît à la fin de phase 4)
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/`, `app/services/people/`, [`../glossary.md`](../glossary.md).
> Plan progressif d'alignement vocabulaire / code / documentation, sans big-bang. Chaque phase est livrable seule, sans casser la précédente.
@@ -71,7 +71,7 @@
- ✅ **Tests à mettre à jour** : remplacer les usages existants.
### Phase 1 — Donations propres
-- `People::PaymentCreator` : retirer la réécriture `item_type: "Donation" → "Payment"` (L92 de `app/services/people/payment_creator.rb`).
+- ✅ `People::PaymentCreator` : la réécriture `item_type: "Donation" → "Payment"` **n’existe plus** sur le chemin simple ; conserver les migrations / specs jusqu’à disparition totale des lignes legacy.
- Data migration : `PaymentLine.where(item_type: "Payment").where("description ILIKE '%don%' OR description = 'Donation'").update_all(item_type: "Donation")`.
- Backfill : pour chaque `Payment.where("donation > 0")`, créer une `PaymentLine` `item_type: "Donation"` si absente.
- Migration DB : `remove_column :payments, :donation`.
diff --git a/docs/payments.md b/docs/payments.md
index 7c7ba88a..d5318b41 100644
--- a/docs/payments.md
+++ b/docs/payments.md
@@ -2,7 +2,7 @@
> **Statut** : stable
> **Public cible** : contributeur
-> **Dernière vérification** : 2026-04-27
+> **Dernière vérification** : 2026-05-01
> **Sources de vérité** : `app/models/payment.rb`, `app/models/payment_line.rb`, `app/services/people/payment_creator.rb`.
> Vocabulaire utilisé : voir [glossary.md](glossary.md).
@@ -17,7 +17,7 @@ Payment (transaction)
├── PaymentLine ── item_type ──> ContributionFormula (legacy: SubscriptionPlan)
├── PaymentLine ── item_type ──> Contribution (legacy: BookOfEntry, rare)
├── PaymentLine ── item_type ──> MembershipType
-└── PaymentLine ── item_type ──> Donation (cible — actuel: "Payment", voir §4)
+└── PaymentLine ── item_type ──> Donation (création actuelle via `PaymentCreator` ; données anciennes peuvent encore avoir `Payment`, voir §4)
```
**Invariant fondamental** : `payment.payment_lines.sum(:amount_cents) == payment.total_cents`.
@@ -58,7 +58,7 @@ Payment#anonymize!
| Renouvellement / catalogue | `"MembershipType"` | `membership_type.id` | rare, surtout pour audit |
| Achat de cotisation | `"ContributionFormula"` | `formula.id` | legacy : `"SubscriptionPlan"` |
| Cotisation existante (réf.) | `"Contribution"` | `contribution.id` | legacy : `"BookOfEntry"` (rare) |
-| Don | `"Donation"` | `payment.id` (provisoire) | actuel : `"Payment"` — voir §4 |
+| Don | `"Donation"` | `payment.id` (provisoire) | création : service ci-dessous ; legacy DB : encore `"Payment"` jusqu’à backfill complet — voir §4 |
### 3.2 Création multi-lignes
@@ -97,28 +97,20 @@ PaymentLine.new(
**Aucune `PaymentLine` ne doit avoir `item_type: "Payment"`**.
-### 4.2 État actuel — dette technique
+### 4.2 État actuel — code vs données legacy
-Le service [`People::PaymentCreator`](../app/services/people/payment_creator.rb) (L87-100) **réécrit** automatiquement les lignes de don :
+**Code (`People::PaymentCreator`)** : la ligne simple utilise `item_type` défaut `"Donation"` et **conserve** ce type pour les dons (`donation_line?` → `item_id` = `payment.id`). Il n’y a plus de réécriture systématique vers `"Payment"` dans ce chemin.
```ruby
-# app/services/people/payment_creator.rb
-def create_single_line(payment)
- line_item_type = item_type.presence || "Donation"
+# app/services/people/payment_creator.rb (extrait)
+payment.payment_lines.create!(
+ item_type: line_item_type, # ex. "Donation"
+ item_id: donation_line?(line_item_type) ? payment.id : item_id,
...
- resolved_item_type = donation_line?(line_item_type) ? "Payment" : line_item_type
- payment.payment_lines.create!(
- item_type: resolved_item_type, # ← écrase "Donation" en "Payment"
- item_id: donation_line?(line_item_type) ? payment.id : item_id,
- ...
- )
-end
+)
```
-**Conséquences** :
-- En base, `payment_lines.item_type = 'Payment'` pour tous les dons existants.
-- Les requêtes de filtrage (`Admin::PaymentsService` L67, `Payment` L44) doivent matcher `item_type = 'Donation'` **OU** `description LIKE '%don%'` pour rétro-compat.
-- Le commentaire de [`Person#create_payment_for_donation`](../app/models/person.rb) L450 (« utiliser Payment comme item_type pour cohérence avec PaymentCreator ») documente le hack mais ne le justifie pas conceptuellement.
+**Données** : des lignes historiques peuvent encore avoir `item_type: "Payment"` jusqu’à application complète des migrations de backfill (`db/migrate/*backfill_donation*`). Les requêtes métier doivent couvrir **Donation** comme canon et **Payment** comme legacy le temps du nettoyage (voir `phase1-donation-fix`).
### 4.3 Plan de migration (phase `phase1-donation-fix`)
@@ -128,7 +120,7 @@ end
.where("description ILIKE ? OR description = ?", "%don%", "Donation")
.update_all(item_type: "Donation")
```
-2. **Suppression du hack** : retirer la réécriture L92 dans `People::PaymentCreator` et le commentaire L450 dans `Person`.
+2. **Nettoyage code résiduel** : retirer toute référence encore basée sur `item_type: "Payment"` pour les dons dans les modèles/helpers commentés ; aligner les specs/factories sur `"Donation"` uniquement.
3. **Mise à jour des requêtes** : simplifier `Payment#with_donations` et `Admin::PaymentsService` à `where(payment_lines: { item_type: "Donation" })`.
4. **Specs à ajuster** : `spec/factories/payments.rb` trait `:with_donation` doit créer une `PaymentLine` `item_type: "Donation"`.
5. **Validation modèle** : ajouter `validates :item_type, inclusion: { in: %w[Membership MembershipType ContributionFormula Contribution Donation] }` sur `PaymentLine`.
diff --git a/lib/tasks/i18n_keys.rake b/lib/tasks/i18n_keys.rake
new file mode 100644
index 00000000..4b852490
--- /dev/null
+++ b/lib/tasks/i18n_keys.rake
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "yaml"
+
+def flatten_keys(hash, prefix = nil)
+ hash.flat_map do |key, value|
+ current = [ prefix, key ].compact.join(".")
+ value.is_a?(Hash) ? flatten_keys(value, current) : [ current ]
+ end
+end
+
+namespace :i18n do
+ desc "Print locale key parity between fr and en"
+ task check_keys: :environment do
+ fr_data = YAML.load_file(Rails.root.join("config/locales/fr.yml"))["fr"] || {}
+ en_data = YAML.load_file(Rails.root.join("config/locales/en.yml"))["en"] || {}
+
+ fr_keys = flatten_keys(fr_data).sort
+ en_keys = flatten_keys(en_data).sort
+
+ missing_in_en = fr_keys - en_keys
+ missing_in_fr = en_keys - fr_keys
+
+ puts "Missing in en (#{missing_in_en.size})"
+ missing_in_en.each { |k| puts " - #{k}" }
+
+ puts "\nMissing in fr (#{missing_in_fr.size})"
+ missing_in_fr.each { |k| puts " - #{k}" }
+
+ if missing_in_en.empty? && missing_in_fr.empty?
+ puts "\nLocale keys are in sync."
+ else
+ puts "\nLocale keys are not in sync."
+ exit(1)
+ end
+ end
+end
diff --git a/spec/forms/admin/user_creation_form_spec.rb b/spec/forms/admin/user_creation_form_spec.rb
index 39b9b0af..c5466a38 100644
--- a/spec/forms/admin/user_creation_form_spec.rb
+++ b/spec/forms/admin/user_creation_form_spec.rb
@@ -91,7 +91,9 @@
result = form.call
expect(result.success?).to be(false)
- expect(result.errors.join(', ')).to include('Invalid data')
+ expect(result.errors.join(', ')).to start_with(
+ I18n.t('admin.users.create.invalid_data_alert', details: '').rstrip
+ )
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index a96e3f45..59d6e569 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -199,10 +199,10 @@
end
end
- context 'with user without person' do
+ context 'with user auto-linked to minimal person' do
let(:user_without_person) { create(:user, person: nil) }
- it 'returns false (no legacy event_attendees)' do
+ it 'returns false when no attendance exists for linked person' do
expect(event.is_user_registered?(user_without_person)).to be false
end
end
diff --git a/spec/models/person_user_architecture_spec.rb b/spec/models/person_user_architecture_spec.rb
index d2ef7ad3..434501e8 100644
--- a/spec/models/person_user_architecture_spec.rb
+++ b/spec/models/person_user_architecture_spec.rb
@@ -34,13 +34,14 @@
end
end
- describe 'User without Person' do
- it 'handles user without person gracefully' do
- user = create(:user, person: nil)
-
- expect(user.first_name).to be_nil
- expect(user.last_name).to be_nil
- expect(user.full_name).to be_nil
+ describe 'User-Person invariant' do
+ it 'auto-creates a minimal person when missing on create' do
+ user = create(:user, person: nil, email_address: 'orphan@example.com')
+
+ expect(user.person).to be_present
+ expect(user.person.first_name).to eq('Web')
+ expect(user.person.last_name).to eq('User')
+ expect(user.person.email).to eq('orphan@example.com')
end
end
end
diff --git a/spec/requests/admin/users_spec.rb b/spec/requests/admin/users_spec.rb
index 0d64408e..f1413459 100644
--- a/spec/requests/admin/users_spec.rb
+++ b/spec/requests/admin/users_spec.rb
@@ -96,7 +96,9 @@
end.not_to change(Person, :count)
expect(response).to have_http_status(:unprocessable_content)
- expect(flash[:alert]).to include('Invalid data')
+ expect(flash[:alert]).to start_with(
+ I18n.t("admin.users.create.invalid_data_alert", details: "").rstrip
+ )
end
end
end
diff --git a/spec/services/account_claim_management/account_claim_creator_spec.rb b/spec/services/account_claim_management/account_claim_creator_spec.rb
index 8f7d367e..f282fd98 100644
--- a/spec/services/account_claim_management/account_claim_creator_spec.rb
+++ b/spec/services/account_claim_management/account_claim_creator_spec.rb
@@ -52,7 +52,7 @@
result = creator.call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'returns failure when user_id is missing' do
diff --git a/spec/services/attendance_list_management/attendance_list_creator_spec.rb b/spec/services/attendance_list_management/attendance_list_creator_spec.rb
index 92af1146..4444f424 100644
--- a/spec/services/attendance_list_management/attendance_list_creator_spec.rb
+++ b/spec/services/attendance_list_management/attendance_list_creator_spec.rb
@@ -42,7 +42,7 @@
result = described_class.new(start_date: Time.current, created_by_id: admin_user.id).call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'fails when created_by user does not exist' do
diff --git a/spec/services/attendance_list_management/attendance_list_deleter_spec.rb b/spec/services/attendance_list_management/attendance_list_deleter_spec.rb
index be737223..e8fb1d26 100644
--- a/spec/services/attendance_list_management/attendance_list_deleter_spec.rb
+++ b/spec/services/attendance_list_management/attendance_list_deleter_spec.rb
@@ -31,7 +31,7 @@
result = described_class.new(deleted_by_id: admin_user.id).call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'fails when attendance list does not exist' do
diff --git a/spec/services/attendance_list_management/attendance_list_updater_spec.rb b/spec/services/attendance_list_management/attendance_list_updater_spec.rb
index 2d5c25de..cc51bfda 100644
--- a/spec/services/attendance_list_management/attendance_list_updater_spec.rb
+++ b/spec/services/attendance_list_management/attendance_list_updater_spec.rb
@@ -43,7 +43,7 @@
result = described_class.new(name: 'Test', updated_by_id: admin_user.id).call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'fails when attendance list does not exist' do
diff --git a/spec/services/attendance_management/attendance_creator_spec.rb b/spec/services/attendance_management/attendance_creator_spec.rb
index d729a6f4..7f1702c9 100644
--- a/spec/services/attendance_management/attendance_creator_spec.rb
+++ b/spec/services/attendance_management/attendance_creator_spec.rb
@@ -54,7 +54,7 @@
result = creator.call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it "returns failure when person doesn't exist" do
diff --git a/spec/services/blog_management/blog_creator_spec.rb b/spec/services/blog_management/blog_creator_spec.rb
index 4829104f..b20d08d7 100644
--- a/spec/services/blog_management/blog_creator_spec.rb
+++ b/spec/services/blog_management/blog_creator_spec.rb
@@ -55,7 +55,7 @@
result = creator.call
expect(result.success?).to be false
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'returns failure when content is missing' do
diff --git a/spec/services/people/account_linker_spec.rb b/spec/services/people/account_linker_spec.rb
index 6c24dda2..7bac8483 100644
--- a/spec/services/people/account_linker_spec.rb
+++ b/spec/services/people/account_linker_spec.rb
@@ -54,7 +54,7 @@
result = described_class.new(user: user).call
expect(result.success?).to be(false)
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
end
end
diff --git a/spec/services/people/account_merger_spec.rb b/spec/services/people/account_merger_spec.rb
index f0aa827e..4da57d8d 100644
--- a/spec/services/people/account_merger_spec.rb
+++ b/spec/services/people/account_merger_spec.rb
@@ -60,7 +60,7 @@
result = described_class.new(source_person: source_person).call
expect(result.success?).to be(false)
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
end
end
diff --git a/spec/services/people/contribution_creator_spec.rb b/spec/services/people/contribution_creator_spec.rb
index 2ce1c161..4c9be963 100644
--- a/spec/services/people/contribution_creator_spec.rb
+++ b/spec/services/people/contribution_creator_spec.rb
@@ -71,7 +71,7 @@
).call
expect(result.success?).to be(false)
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'fails when plan missing' do
diff --git a/spec/services/people/contribution_upgrader_spec.rb b/spec/services/people/contribution_upgrader_spec.rb
index 3eaf96e6..015c6ad8 100644
--- a/spec/services/people/contribution_upgrader_spec.rb
+++ b/spec/services/people/contribution_upgrader_spec.rb
@@ -60,7 +60,7 @@
).call
expect(result.success?).to be(false)
- expect(result.message).to include('Invalid data')
+ expect(result.message).to include(I18n.t('services.validation.invalid_data'))
end
it 'fails when recorded_by missing' do
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
deleted file mode 100644
index cee29fd2..00000000
--- a/test/application_system_test_case.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require "test_helper"
-
-class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
-end
diff --git a/test/controllers/partners_controller_test.rb b/test/controllers/partners_controller_test.rb
deleted file mode 100644
index 5277d305..00000000
--- a/test/controllers/partners_controller_test.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "test_helper"
-
-class PartnersControllerTest < ActionDispatch::IntegrationTest
- test "returns partners partial as turbo stream" do
- get partners_url(format: :turbo_stream)
-
- assert_response :success
- assert_equal "text/vnd.turbo-stream.html", response.media_type
- assert_includes response.body, "Ville de Toulouse"
- end
-
- test "redirects to about page for html requests" do
- get partners_url
-
- assert_redirected_to page_path("about")
- end
-end
diff --git a/test/system/about_page_test.rb b/test/system/about_page_test.rb
deleted file mode 100644
index 48f65875..00000000
--- a/test/system/about_page_test.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require "application_system_test_case"
-
-class AboutPageTest < ApplicationSystemTestCase
- test "shows partners on about page" do
- visit page_path("about")
-
- assert_selector "h1", text: "À propos du Circographe"
- assert_selector "[data-controller='timeline'] .timeline-step", minimum: 3, visible: :all, wait: 5
- end
-end
diff --git a/test/system/association_page_test.rb b/test/system/association_page_test.rb
deleted file mode 100644
index e5be34c2..00000000
--- a/test/system/association_page_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require "application_system_test_case"
-
-class AssociationPageTest < ApplicationSystemTestCase
- test "association page exposes anchor sections" do
- visit page_path("association")
-
- assert_selector "h1", text: /Bienvenue au Circographe/
- assert_selector "section#le-cirque", wait: 5, visible: :all
- assert_selector "section#les-arts-graphiques", visible: :all
- assert_selector "section#fonctionnement", visible: :all
- end
-end
diff --git a/test/system/become_member_page_test.rb b/test/system/become_member_page_test.rb
deleted file mode 100644
index cc482af6..00000000
--- a/test/system/become_member_page_test.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "application_system_test_case"
-
-class BecomeMemberPageTest < ApplicationSystemTestCase
- test "adhesion page shows timeline and checklist" do
- visit page_path("become_member")
-
- assert_selector "h1", text: /Adhérer/
- assert_selector "[data-controller='timeline'] .timeline-step", minimum: 3, wait: 5
- assert_selector "[data-controller='checklist'] li", minimum: 1, visible: :all
- end
-end
diff --git a/test/system/contact_page_test.rb b/test/system/contact_page_test.rb
deleted file mode 100644
index 1c817883..00000000
--- a/test/system/contact_page_test.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "application_system_test_case"
-
-class ContactPageTest < ApplicationSystemTestCase
- test "contact page renders form turbo frame and FAQ" do
- visit page_path("contact_us")
-
- assert_selector "h1", text: /Contact/
- assert_selector "turbo-frame#contact_form", wait: 5
- assert_selector "[data-controller='accordion']", visible: :all, wait: 5
- end
-end
diff --git a/test/system/faq_page_test.rb b/test/system/faq_page_test.rb
deleted file mode 100644
index a0c67051..00000000
--- a/test/system/faq_page_test.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "application_system_test_case"
-
-class FaqPageTest < ApplicationSystemTestCase
- test "faq page shows sections and anchors" do
- visit page_path("faq")
-
- assert_selector "h1", text: /Questions fréquentes/
- assert_selector "a[href='#faq-adhesion']"
- assert_selector "a", text: "Adhésion", wait: 5
- assert_selector "a", text: "Contact"
- assert_selector "a", text: "Vie du lieu"
- end
-end
diff --git a/test/system/home_page_test.rb b/test/system/home_page_test.rb
deleted file mode 100644
index 9d5d34f8..00000000
--- a/test/system/home_page_test.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require "application_system_test_case"
-
-class HomePageTest < ApplicationSystemTestCase
- test "home page shows upcoming events frame" do
- visit root_path
-
- assert_selector "h1", text: /Circographe/i
- assert_selector "turbo-frame#events_upcoming"
- assert_selector "section", text: /Horaires d'ouverture/
- end
-end