Statut : stable Public cible : contributeur 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
Dernière revue contenu: 2025-11-09
Services: Consolidés (People::* pour adhésions/paiements/cotisations ; plusieurs *Management retirés côté admin)
Controllers: Admin simplifiés (CRUD inline pour Events, MembershipTypes, ContributionFormulas)
Les controllers sont MINIMALISTES et délèguent TOUT vers les services.
Les services suivent le pattern Service Object avec ActiveModel::Model :
- Validation des paramètres
- Délégation vers la logique métier dans les modèles (Person, etc.)
- Instrumentation pour audit (ActiveSupport::Notifications)
- Gestion d'erreurs standardisée
- 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 (email, mot de passe, rôle) qui délègue tous ses attributs de profil à
Personviadelegate. Données : toutUsera unePerson(belongs_to :person, NOT NULL) ; à la création web sans fiche existante, unePersonminimale est créée par callback surUser. - Règles clés :
- Créer/éditer la fiche métier via
People::Register/People::PersonCreator; le compte web est créé viaPeople::UserAccountCreatorsi besoin. - Rattacher un compte existant à une fiche CRM :
People::AttachUserToPerson(nominal) ;People::AccountLinkerencapsule attach + nettoyage éventuel (People::AccountMerger). - Supprimer un
Userne détruit pas laPerson. CôtéPerson,has_one :user, dependent: :restrict_with_errorempêche une suppression de fiche incompatible tant qu’unUserexiste — passer par archive / RGPD. - Supprimer une
Personpasse parSoftDeletable(Person#archive!) avec garde-fous financiers (has_financial_data?). - Toutes les opérations financières (
People::Payment*,People::Contribution*,People::Register) travaillent exclusivement surPerson.
- Créer/éditer la fiche métier via
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 explicite et auditée (
AttachUserToPerson/AccountLinker) quand un compte web doit être relié à une fiche existante.
People::MembershipCreator- Création d'adhésionsPeople::MembershipUpgrader- Upgrade d'adhésions (Basic → Circus)People::MembershipUpdater- Mise à jour d'adhésion (type, dates)People::MembershipDeactivator- Désactivation d'adhésion (status: :inactive)
Utilisé dans: Admin::MembershipsController (create, update, destroy)
People::ContributionCreator— création de cotisations.People::ContributionUpgrader— upgrade / prorata Trimestre → Annuel.
Utilisé dans :
Admin::ContributionFormulasController—createAdmin::ContributionsController—upgrade
People::PaymentCreator— paiements simples et multi-lignes (incluant don).People::PaymentUpdater/PaymentCanceller/PaymentRestorer— utilisés depuis l'admin.
Utilisé dans :
Admin::PaymentsController(create, update, destroy)Admin::DonationsController(create)Admin::Users::PaymentsController(create, update, destroy viaPeople::PaymentCreatormulti-lignes)
Donations — état actuel et cible
- Cible : une donation est une
PaymentLineavecitem_type: "Donation"etitem_idadapté (souventpayment.idpour les dons « libres »). Aucune nouvelle ligne ne doit utiliseritem_type: "Payment"pour un don. - Code actuel :
People::PaymentCreatorgardeitem_type: "Donation"sur la ligne simple lorsque le flux est un don (donation_line?). Les anciennes lignes en base peuvent encore êtreitem_type: "Payment"— backfill / reporting : voir../payments.mdetphase1-donation-fix. - Comportement attendu côté appelant : passer
item_type: "Donation"(ou laisser le défaut du service) ; fournir montants cohérents avectotal_cents. - Validation : la somme des
payment_linesdoit égalertotal_cents; sinon,failure.
People::AttachUserToPerson: rattache unUserà unePersoncible (refuse si la cible a déjà un autreUser), instrumentationpeople.user_attached.People::AccountLinker: orchestration (attach viaAttachUserToPerson, merge optionnel de l’ancienne fiche avecPeople::AccountMerger). Événementpeople.account_linked.- Utilisé aussi par
AccountClaimManagement::AccountClaimConfirmer(attach direct possible) et scripts (scripts/fix_person_user_merge.rb).
- Ancien namespace conservé uniquement pour compatibilité. Les nouvelles fusions / liaisons doivent passer par
People::AttachUserToPerson,People::AccountLinkerouPeople::AccountMerger.
UserDeleter- Suppression d'utilisateurs (Person)UserUpdater- Mise à jour User/Person (newsletter, rôles)
- Flux admin géré en CRUD inline par
Admin::EventsController(validation au modèle,Event#titlevirtuel)
AccountClaimManagement::AccountClaimCreator- Création de demandes de réclamationAccountClaimManagement::AccountClaimConfirmer- Confirmation et fusion de comptes
AttendanceCreator- Création de présences
Utilisé dans:
Admin::AttendancesController(create)
AttendanceListCreator- Création de listes de présence (avec gestion dates par défaut)AttendanceListUpdater- Mise à jour de listes de présenceAttendanceListDeleter- Suppression de listes de présence
Utilisé dans:
Admin::AttendanceListsController(create, update, destroy)
MemberNumberSuggester- Suggestion de numéro d'adhérentMemberNumberChanger- Changement de numéro d'adhérent (avec validation format, unicité, historique)
Utilisé dans:
Admin::MemberNumbersController(suggest, change)
- Flux admin géré en CRUD inline par
Admin::ContributionFormulasController
- Flux admin géré en CRUD inline par
Admin::BlogsController
- Géré directement par
Admin::MembershipTypesController(CRUD inline)
- Mise à jour via cache dans
Admin::OpeningHoursController(à migrer vers un modèleSettingsi besoin)
- Public:
People::NewsletterSignup(instrumentation:people.newsletter_signed_up,people.newsletter_signup.skipped,people.newsletter_signup.failed) - Authentifié:
NewsletterManagement::NewsletterUpdater
*Services non-standard (pas dans Management):
Admin::PaymentsService- Helper service pour filtres et statistiques (utilisé dansAdmin::PaymentsController#index)Admin::DashboardStatisticsService- Service pour calculer les statistiques du dashboard admin (utilisé dansAdmin::UsersController#index)MemberManagementService- Service legacy pour génération numéros d'adhérent (utilisé parMemberNumberManagement::*)People::NewsletterSignup- Inscription newsletter publique (remplaceNewsletterSignupService)Web::UserRegistration- Service pour inscription web (utilisé dansRegistrationsController)
Note: Ces services ne suivent pas le pattern DomainManagement standard mais sont acceptables car:
- Services helpers pour filtres/statistiques (Admin::PaymentsService, Admin::DashboardStatisticsService)
- Services legacy maintenus pour compatibilité
- Services web spécifiques (Web::UserRegistration)
L'application utilise ViewComponent pour les composants réutilisables (21 composants actifs).
Structure:
app/components/admin/users/- Composants admin users (badges, actions, display)app/components/admin/payments/- Composants payments (summary, display, actions)app/components/ui/- Composants UI réutilisables
Bonnes pratiques:
- Un composant = un fichier Ruby + un template ERB
- Namespace par domaine (Admin::Users::, Admin::Payments::)
- Logique de présentation uniquement (pas de logique métier)
Helpers communs:
PaymentMethodsHelper#payment_method_options(include_pending: false)— source unique des options de paiement (réutilisée dans les vues admin).
Voir: overview.md pour détails complets sur ViewComponents.
module DomainManagement
class ResourceCreator
include ActiveModel::Model
include ActiveModel::Attributes
attribute :resource
attribute :param_id, :integer
attribute :recorded_by_id, :integer
validates :resource, presence: true
validates :param_id, presence: true
validates :recorded_by_id, presence: true
def call
return failure("Invalid data") unless valid?
begin
# 1. Trouver les ressources
param = Param.find(param_id)
recorded_by = User.find(recorded_by_id)
# 2. Déléguer vers la logique métier dans le modèle
result = resource.create_something!(
param,
recorded_by: recorded_by
)
# 3. Instrumentation pour audit
ActiveSupport::Notifications.instrument(
"something.created",
resource_id: resource.id,
param_id: param.id,
recorded_by_id: recorded_by_id
)
success(result[:something], result[:payment])
rescue ActiveRecord::RecordNotFound => e
failure("Record not found: #{e.message}")
rescue => e
Rails.logger.error "[ResourceCreator] Error: #{e.message}"
failure("Error: #{e.message}")
end
end
private
def success(resource, payment)
OpenStruct.new(
success?: true,
resource: resource,
payment: payment,
message: "Resource created successfully"
)
end
def failure(message)
OpenStruct.new(
success?: false,
errors: [message],
message: message
)
end
end
end-
Les services NE font PAS de logique métier complexe - Ils délèguent vers les modèles
-
La logique métier reste dans les modèles (
Person#create_membership!,Person#create_contribution!, etc.) -
Les services ajoutent uniquement:
- Validation des paramètres
- Instrumentation (audit)
- Gestion d'erreurs standardisée
- Interface cohérente pour les controllers
-
NE PAS créer de services qui dupliquent la logique métier - Toujours déléguer vers les modèles
def create
creator = DomainManagement::ResourceCreator.new(
resource: @resource,
param_id: params[:param_id],
recorded_by_id: Current.user.id
)
result = creator.call
if result.success?
redirect_to path, notice: "Success"
else
redirect_to path, alert: result.message
end
end