Modèle de domaine
Le domaine s'articule autour de quatre familles d'entités : les acteurs (User, Adherent, Prestataire, Comptoir, Groupe, Siège), les comptes (Account*), les flux et opérations (Flux*, Operation*), et les artefacts financiers (SepaMandate, Recurring*, SSA*, HelloAsso, PendingPaymentData).
Diagramme d'entités (vue financière)
classDiagram
direction LR
class Account {
<<MappedSuperclass>>
+UUID id
+decimal balance
+postPersist()
}
class AccountAdherent
class AccountPrestataire
class AccountComptoir
class AccountGroupe
class AccountSiege
Account <|-- AccountAdherent
Account <|-- AccountPrestataire
Account <|-- AccountComptoir
Account <|-- AccountGroupe
Account <|-- AccountSiege
class Flux {
<<abstract>>
+UUID id
+decimal montant
+datetime createdAt
+string hash
+string reference
}
class AchatMonnaie
class Retrait
class Change
class Don
class Cotisation
class Transaction
Flux <|-- AchatMonnaie
Flux <|-- Retrait
Flux <|-- Change
Flux <|-- Don
Flux <|-- Cotisation
Flux <|-- Transaction
class Operation {
<<abstract>>
+UUID id
+decimal montant
+string hash
}
Account "1" --> "*" Operation : a
Flux "1" --> "*" Operation : genere
class SepaMandate {
+UUID id
+string umr
+string status
+string ibanEncrypted
+datetime signedAt
}
class RecurringSubscriptionRequest {
<<abstract>>
}
class RecurringPurchaseRequest
SepaMandate "1" --> "*" RecurringPurchaseRequest : couvre
SepaMandate "0..1" --> "*" RecurringSubscriptionRequest : couvre
class SolidoumeParameter {
+UUID id
+string strategy
+decimal targetAmount
+decimal rate
}
Vue financière simplifiée (héritages STI Flux et MappedSuperclass Account). UMR au format KOHINOS-YYYYMMDD-XXXXXXXX.
Comptes (Account*)
Account est une MappedSuperclass (pas une table) dont chaque type d'acteur
hérite avec sa propre table. Tous les comptes partagent :
- Un identifiant UUID v4.
- Un solde en
NUMERIC(10,2). - Un lifecycle callback
postPersistqui crée le compte automatiquement à la création de l'entité métier (adhérent, prestataire…). - Une limite de découvert configurable globalement (
GlobalParameter).
Le solde est recalculé en temps réel depuis les opérations. Ne jamais le cacher
en dehors d'une transaction atomique avec un lock pessimiste :
$em->find(Account::class, $id, LockMode::PESSIMISTIC_WRITE).
Flux & Opérations
Le Flux est le pivot monétaire de toute transaction. Il est en
STI (single table inheritance) avec un discriminator (achat, retrait, change, don,
cotisation, transaction). Chaque flux génère au moins deux Operation
(une débit, une crédit) qui appliquent l'effet sur les comptes.
Chaque Operation est signée cryptographiquement en référence à la précédente
(chaînage), formant une chaîne vérifiable de toutes les écritures financières.
Patterns de référence (champ reference)
| Cas | Pattern |
|---|---|
| SSA prélèvement | Solidoume prélèvement {month} |
| SSA redistribution | Solidoume {month} |
| SSA cotisation participant | Solidoume cotisation {month} |
| Achat récurrent SEPA | Achat récurrent {month} |
| Cotisation annuelle MLC | Cotisation MLC {year} |
SEPA
- UMR (Unique Mandate Reference) au format
KOHINOS-YYYYMMDD-XXXXXXXX(8 premiers caractères de l'UUID). - Workflow :
pending → validated → active → cancelled / revoked. - IBAN chiffrés en base (AES-256-CBC interne, anciennement
SpecShaper/GdprBundle). - Signatures libsodium uniquement (Ed25519). Pas d'
openssl_*. - Jour de prélèvement borné à [1..28] pour cohérence mensuelle (cf. ADR-0002).
- Anti-double-traitement :
last_processed_monthau formatYYYYMMsur chaque mandat. - Annulation atomique : révoquer un mandat annule, dans la même transaction, toutes ses
RecurringPurchaseRequest.
SSA — fonds SSA
Un SolidoumeParameter définit un fonds (taux de prélèvement, stratégie de redistribution,
SSA principal vs régional). Plusieurs fonds coexistent. La stratégie est une instance de
RedistributionStrategy :
CalculatedStrategy— montant cible par participant, le surplus reste dans le fonds.ProportionalStrategy— répartition selon les achats du mois.FixedAmountStrategy— montant fixe par participant.
Enums & types
- Statuts en enums PHP 8.1 backed string (
src/Enum/) :SepaMandateStatus,RecurringRequestStatus,RecurringPurchaseStatus,GdprRequestStatus,EmailLogStatus,IbanChangeRequestStatus. Stratégie hybrideenumType: propriété Doctrine typée enum (colonne VARCHAR inchangée, aucune migration), API string conservée (getStatus()/setStatus(string)validant) + accesseurs typésgetStatusEnum()/setStatusEnum(). Lesconst STATUS_*restent des littéraux synchronisés (verrouillé parStatusEnumsTest). FluxTypeexclu de la migration enum : la colonneflux.typestocke les 26 discriminateurs SINGLE_TABLE (dénormalisés enprePersist), pas les 10 catégoriesFlux::TYPE_*.- Montants en
NUMERIC(10,2)en base. Côté PHP, certains getters retournentstring, d'autresfloat: cast explicite avant comparaison ;bcmathpour les opérations sensibles. - UUID via
#[ORM\Column(type: 'uuid')]etramsey/uuid-doctrine.
Invariants à toujours protéger
- 1 eMLc émis = 1 € en réserve (conservation).
- Toute modification de solde = 1 lock pessimiste + 1
Operationdans la même transaction. - Pas d'écriture dans
operation_*sansFluxparent en base (FKflux_id). - Pas de modification de
flux.hashouoperation.hashaprès création (chaînage). - Mandat
active⇒signed_at IS NOT NULL. Mandatcancelled⇒cancelled_at IS NOT NULL. last_processed_monthempêche le double-prélèvement SEPA.