Reconduction tacite — la date de fin persistée n'est jamais étendue, le quittancement s'arrête silencieusement à l'échéance.
Analyse du code (adb-accounting, adb-contracts, adb-model) confrontée à la matrice comptable documentée et aux règles métier de l'administration de biens en France. Chaque constat a été contre-vérifié par un agent adversarial indépendant, puis annoté du retour de l'équipe de développement.
Le noyau comptable est sain ; la périphérie est dangereuse. La partie double, l'idempotence du quittancement mensuel, la matrice d'écritures des flux cœur et le principe de reversement « sur encaissé » sont corrects. Les défauts graves se concentrent sur les chaînes transverses : paiements fournisseurs, GLI, prélèvements SEPA, clôture d'exercice, honoraires/TVA et relances.
The accounting core is sound; the periphery is dangerous. Double-entry, monthly quittancement idempotency, the posting matrix for the core flows and the cash-basis owner-payout principle are correct. The serious defects cluster on the cross-cutting chains: supplier payments, GLI, SEPA direct debit, fiscal-year close, fees/VAT and dunning.
Le retour de l'équipe apporte l'intention que l'audit ne pouvait pas connaître : sur 102 constats annotés, 30 sont confirmés avec un correctif précis, 19 sont des limites assumées (MVP/v1), 27 des choix de conception arbitrés, 17 sont contestés avec argument, et 9 relèvent d'un arbitrage métier. Les constats restent affichés tels quels : la note du dev s'ajoute, elle ne les remplace pas.
The team's feedback supplies the intent the audit could not know: of 102 annotated findings, 30 are confirmed with a precise fix, 19 are accepted limitations (MVP/v1), 27 are deliberate design choices, 17 are disputed with an argument, and 9 need a business decision. Findings are shown unchanged: the engineer note is added, it does not replace them.
Reconduction tacite — la date de fin persistée n'est jamais étendue, le quittancement s'arrête silencieusement à l'échéance.
Relances jamais envoyées — l'envoi e-mail est inactif alors que l'avis est enregistré comme envoyé.
Indemnité GLI jamais reversée au bailleur — transfert GLI_TO_OWNER inatteignable.
Paiement fournisseur lié à un mandat : banque créditée 2×, propriétaire double-débité.
SUPPLIER_PAYMENT_ISSUED : banque créditée 2× et numéros de ligne FEC dupliqués.
Reprise créditrice non-cash : l'agence reverse au propriétaire de l'argent jamais encaissé.
adb-accounting/.../model/PostingRuleFactory.java:421-440
Supprimer la branche if (debitAccountOwner != null). La duplication des numéros de ligne se résout d'elle-même (1,2,2,3 → 1,2). Base vérifiée par le dev : aucune correction de données.
Remove the if (debitAccountOwner != null) branch. Line-number duplication resolves itself (1,2,2,3 → 1,2). DB checked by the engineer: no data correction needed.
adb-accounting/.../service/impl/PostSettlementsProcessorImpl.java:52
Retirer MANUAL_ENTRY de ALLOWED_CREDIT_EVENT_TYPE.
Remove MANUAL_ENTRY from ALLOWED_CREDIT_EVENT_TYPE.
adb/adb-model/.../model/contract/InsuranceOption.java:78
difference > 0 → difference >= 0.
difference > 0 → difference >= 0.
adb-accounting/.../insurance/InsuranceContractInvoiceCreationStrategy.java:366,378
Comparer le YearMonth complet au lieu du seul mois.
Compare the full YearMonth instead of the month alone.
Le dump seed/production infra/seeds/data/adb-accounting/journals.json contient 13 journaux, dont TAKEOVER_BALANCE (11), SUPPLIER_TO_OWNER (12) et GLI_INDEMNITIES (13). L'hypothèse est donc confirmée : ces trois-là existent bien en environnement établi, et seuls les déploiements neufs (amorcés depuis journals.csv, 10 lignes) sont exposés. En revanche GLI_TO_OWNER n'existe nulle part, hors fixtures de test — alors que AccountingEventType.java:140 le référence et que JournalHandler lève IllegalArgumentException("No journal found…"). Le reversement GLI au bailleur a donc deux blocages indépendants (C3) : le chemin de code inatteignable et le journal absent. Corriger l'un sans l'autre ne ferait que déplacer l'échec.
The seed/production dump infra/seeds/data/adb-accounting/journals.json holds 13 journals, including TAKEOVER_BALANCE (11), SUPPLIER_TO_OWNER (12) and GLI_INDEMNITIES (13). The hypothesis is confirmed: those three do exist in established environments, and only fresh deployments (seeded from journals.csv, 10 rows) are exposed. However GLI_TO_OWNER exists nowhere outside test fixtures — while AccountingEventType.java:140 references it and JournalHandler throws IllegalArgumentException("No journal found…"). So the GLI payout to the landlord has two independent blockers (C3): the unreachable code path and the missing journal row. Fixing one without the other only moves the failure.
isAllowedManualEntry (PostSettlementsProcessorImpl.java:201-206) est aujourd'hui du code mort : MANUAL_ENTRY figurant déjà dans ALLOWED_CREDIT_EVENT_TYPE, la condition || laisse passer toute écriture manuelle quelle que soit sa nature. Le retirer de la première liste réactive le garde-fou par nature : les reprises CREDITOR_CASH, DEBITOR* et GLI_INDEMNITIES_RECEIVED continuent de déclencher le transfert propriétaire ; seul CREDITOR_NOT_CASH, absent de cette liste, cesse de le faire. À noter : le même correctif neutralise aussi deux constats frères (transfert propriétaire sur crédit manuel côté paiements et côté charges).
isAllowedManualEntry (PostSettlementsProcessorImpl.java:201-206) is currently dead code: since MANUAL_ENTRY is already in ALLOWED_CREDIT_EVENT_TYPE, the || lets any manual entry through whatever its nature. Removing it from the first list re-activates the nature gate: CREDITOR_CASH, DEBITOR* and GLI_INDEMNITIES_RECEIVED takeovers keep triggering the owner transfer; only CREDITOR_NOT_CASH, absent from that list, stops. Note: the same fix also neutralises two sibling findings (owner transfer on manual credit, on the payments and charges sides).
Pour les locataires dont le mode d'envoi est l'email, aucune relance ni mise en demeure ne part réellement : l'envoi a été désactivé dans le système, et le brouillon restant contient un texte de remplissage. Pourtant, le système génère le courrier en PDF, enregistre la date de relance et fait monter le niveau d'escalade comme si l'envoi avait eu lieu. Concrètement, un locataire peut être poussé jusqu'au contentieux sur la base de relances qu'il n'a jamais reçues, ce qui expose l'agence à un risque juridique sérieux et fausse tout le suivi des impayés.
Les e-mails de relance ne sont jamais envoyés en silence, tout en étant enregistrés comme envoyés
DunningServiceImpl.sendNoticeEmail() a l'appel réel à EmailService mis en commentaire et retourne Mono.empty() ; le builder restant utilise un objet factice "pay" et un corps "<html>Content</html>". Parallèlement, processNoticeSentEvent génère/stocke toujours le PDF, DunningRepositoryImpl inscrit previousDunningNoticeDate=now pour chaque locataire disposant d'un mode de distribution, et les compteurs de DunningNotice comptabilisent l'e-mail. Un locataire dont le mode de distribution est EMAIL ne reçoit jamais aucune relance/mise en demeure, mais le système enregistre l'avis comme envoyé, le niveau comme escaladé, et la date de l'avis sert à filtrer/justifier une escalade ultérieure — un contentieux pourrait être engagé sur la base d'avis jamais délivrés.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:539adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:419
Contre-vérification — DunningServiceImpl.java:539-541 : emailService.send est mis en commentaire ; sendNoticeEmail retourne Mono.empty() avec l'objet factice "pay". Aucun autre émetteur n'existe : adb-notifications ne gère que les e-mails d'appel de loyer et d'attestation ; l'UI se contente de POSTer /contracts/dunnings/notices et affiche le canal e-mail comme envoyé (dunning-notice-details-table.component.html:531). Parallèlement, sendDunningNotice (lignes 161-187) comptabilise l'e-mail (countTotals:371) et DunningRepositoryImpl.java:418-419 inscrit previousDunningNoticeDate=now indépendamment de la délivrance ; cette date pilote les filtres (lignes 585-621). La doc affirme même que SendGrid délivre l'e-mail de relance (adb-doc/docs/services/adb-contracts.md:241). Les locataires en EMAIL seul ne reçoivent jamais les avis enregistrés comme envoyés.
De mémoire, l'envoi d'e-mails était prévu pour une v2 du flux de relance ; la première version ne faisait que générer les fichiers d'avis. Un module de communication séparé (adb-communications, utilisant Twilio) était développé par un autre dev, parti depuis, et n'a jamais été pleinement livré en production. Relance et communication fonctionnaient ensemble au moins pour les cas positifs en développement, mais la priorité s'est ensuite éloignée de la relance. Piste de reprise : localiser l'implémentation adb-communications, vérifier l'intégration relance et la restaurer (quelques mises à jour de code + un peu d'infra). Je peux aider à investiguer et reconnecter ce module.
Quand le solde d'un locataire passe en sa faveur (par exemple il devait 500 EUR et en verse 600), la mise à jour est ignorée par le module de relance. Le locataire reste alors dans la liste des impayés avec son ancienne dette affichée, et peut recevoir de nouvelles relances totalement injustifiées. Les tests internes montrent que le système était conçu pour retirer ces locataires de la liste, mais ce comportement ne fonctionne pas en pratique. Risque : relances abusives envers des locataires à jour, dégradation de la relation client et perte de crédibilité des courriers.
Les mises à jour de solde qui deviennent négatives sont abandonnées, laissant à tort des locataires listés comme débiteurs avec un solde positif obsolète
processBalanceEvent filtre les événements de solde auxiliaire entrants avec `compareTo(ZERO) >= 0`, si bien que tout événement où le solde du locataire est devenu négatif (trop-perçu, ajustement de crédit : ex. le locataire doit 500, paie 600, solde -100) est écarté et le document de relance n'est jamais mis à jour. La version stockée conserve le dernier solde positif, et findAll/find n'exigent que `versions.1.balance.amount > 0`, de sorte que le locataire reste dans la liste de relance affichant l'ancienne dette et peut recevoir d'autres relances (infondées). Le repository prend explicitement en charge les upserts à solde négatif — DunningRepositoryTest.testUpsertBalanceTurnedNegative fait un upsert de -170 après +230 et attend que la relance disparaisse de findAll — prouvant que le filtre au niveau service met en échec la transition prévue.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:225adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:354adb-contracts/src/test/java/fr/lifeconnect/adb/contract/repo/DunningRepositoryTest.java:186
Contre-vérification — Le filtre à DunningServiceImpl.java:225 (>= 0) écarte les événements à solde négatif ; le solde est signé débit-crédit (BalanceCalculator.java:36), donc les locataires en trop-perçu émettent des événements négatifs (le trigger Atlas se déclenche sur toute mise à jour de ledger_auxiliary_account, atlas-triggers.md:45). saveDunning/upsert n'est atteignable que depuis processBalanceEvent:245 ; aucun autre chemin ne rafraîchit le solde (sendDunningNotice le saute "pour la performance", DunningServiceImpl.java:183). La logique de merge du repo gère le passage positif-vers-négatif (DunningRepositoryImpl.java:354-357) et testUpsertBalanceTurnedNegative affirme que la relance disparaît de findAll — une transition inatteignable en production. Le solde positif obsolète maintient les locataires créditeurs dans la liste de relance. Élevé, pas critique : les paiements à zéro exact passent, les avis sont déclenchés par l'utilisateur, l'état se corrige de lui-même quand le solde redevient positif.
Ce n'est pas un bug : le solde est calculé par la formule DÉBIT - CRÉDIT. Un solde positif signifie donc que le débit excède le crédit, c'est-à-dire une dette.
Lorsqu'un remboursement lié à la garantie loyers impayés (GLI) est saisi avec la nature « assurance » ou « propriétaire », le paiement est bien enregistré et marqué comme comptabilisé, mais aucune écriture n'est passée : l'encaissement en banque n'apparaît pas, le compte du locataire n'est pas soldé et la dette envers l'assureur n'est pas constatée. Seule la nature « agence » fonctionne correctement, alors que l'écran propose les trois choix. De l'argent encaissé sans aucune trace au grand livre : rapprochements bancaires faux, soldes locataires faux, et exposition en cas de contrôle.
Le remboursement locataire GLI avec nature INSURANCE ou OWNER n'enregistre aucune écriture comptable
L'UI (gli-tenant-payback) propose les trois natures du catalogue TENANT_PAYBACK_GLI_AGENCY/_INSURANCE/_OWNER. InsurancePaybackPaymentEvent est construit avec postingRuleGenerator=FALSE (de sorte que l'écriture GLI_PAYMENT_PAYBACK 405100->512101 documentée dans le matrice des événements n'est jamais passée), et friends() ne crée d'événements secondaires QUE pour la nature AGENCY. Pour INSURANCE et OWNER, le paiement est traité et COMPTABILISÉ (AbstractPaymentStrategy.update fixe paymentState=BOOKED sur PROCESSED) sans aucune écriture au grand livre : l'encaissement bancaire n'est pas comptabilisé, le locataire 411100 n'est pas crédité, le solde douteux 416100 n'est pas apuré, et la dette envers l'assureur n'apparaît jamais — de la trésorerie reçue sans aucune trace au grand livre.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InsurancePaybackPaymentEvent.java:66adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Payment.java:250adb-ui/src/app/content/pages/accounting/gli-tenant-payback/gli-tenant-payback.component.ts:115
Contre-vérification — Re-dérivé à partir du code. L'UI propose les 3 natures (gli-tenant-payback.component.ts:115) et envoie le choix comme compensationType (accounting.mapper.ts:241-242). Tout compensationType non nul construit InsurancePaybackPaymentEvent (AbstractPaymentStrategy.java:58-62) avec postingRuleGenerator=FALSE (InsurancePaybackPaymentEvent.java:56), donc tous les handlers de posting sont sautés (PostingRuleHandler.java:24, EntryHandler.java:38) ; friends() n'ajoute des événements secondaires que pour TENANT_PAYBACK_GLI_AGENCY (:66). Aucun validateur ne vérifie la nature (PreValidationHandler ne vérifie que les contrats d'assurance liés). La chaîne se termine PROCESSED (DefaultHandlerChain.java:58) et le paiement est mis à BOOKED (AbstractPaymentStrategy.update:37-38). Le grep confirme que TENANT_PAYBACK_GLI_INSURANCE/_OWNER ne sont gérés nulle part ; Payment.createPostingRule n'a pas de cas GLI_PAYMENT_PAYBACK, donc l'écriture documentée 405100/512101 n'est jamais passée. La sévérité élevée est correcte.
Ce n'est pas une erreur : la fonctionnalité n'a été conçue que pour AGENCY, et le front se contente d'afficher les valeurs de catalogue existantes.
Le courrier de relance simple envoyé aux locataires d'habitation affirme que le bail prévoit que « toute relance sera facturée ». Or, en bail d'habitation, la loi rend nulle toute clause facturant les frais de relance ou de mise en demeure au locataire ; seuls les actes d'huissier sont récupérables. Le courrier s'appuie donc sur une clause sans valeur et annonce une facturation illégale. De plus, les opérations manuelles permettant de débiter le locataire de frais de commandement ou de frais juridiques sont utilisables sans aucun garde-fou, ce qui facilite des facturations indues et expose l'agence à des contestations et sanctions.
La lettre de relance de niveau 1 indique aux locataires en habitation que chaque relance leur sera facturée
Le template de relance de niveau 1 en production (RELANCE SIMPLE) indique, pour un « bail d'habitation », que le bail stipule que « toute relance sera facturée » au locataire. Au titre de la loi 89-462 art. 4 p), toute clause facturant les frais de relance/mise en demeure à un locataire en habitation est réputée non écrite (nulle) ; seuls les actes d'huissier réels (commandement de payer, art. 24) sont recouvrables. La lettre invoque donc une clause nulle et menace (ou annonce) une facturation illégale ; combiné aux événements manuels librement utilisables SUMMONS_TO_PAY_DUE_BY_TENANT (706645) et LEGAL_COSTS_DUE_BY_TENANT (706640) qui débitent le locataire 411100 sans garde-fou/avertissement les restreignant aux véritables frais d'huissier/de justice, la plateforme facilite la facturation aux locataires de simples relances. La même phrase code aussi en dur l'échéance de loyer « le 1er de chaque mois » quelle que soit la véritable modalité de paiement du contrat (et contient la coquille « daft » pour « doit »).
adb-charts/releases/templates/dunning-level1.ftlh:534adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1235
Contre-vérification — Texto à adb-charts/releases/templates/dunning-level1.ftlh:534 : « clauses de votre bail d'habitation ... daft intervenir le 1er de chaque mois et que toute relance sera facturée » — texte statique, sans condition FreeMarker. La sélection du template se fait uniquement par niveau de relance (DunningServiceImpl.java:400, flux live à :214/:279), donc chaque lettre de niveau 1, quel que soit le type de bail réel ou le jour de paiement, affirme cette clause. La loi 89-462 art. 4 p) est correctement énoncée : les clauses de frais de relance sont nulles pour les baux d'habitation. La sous-affirmation sur les événements manuels (RentalContract.java:1235-1248, la doc confirme Manual? oui, 411100 D) est plus faible — ces événements couvrent légitimement commandement/frais de justice — mais le défaut central de la lettre subsiste.
Exact, c'est codé en dur dans le template.
Le modèle de la mise en demeure de niveau 3 mentionne en dur, dans le paragraphe clé, un bailleur unique (« Société V FUND IMMO CORE »), alors que le nom du vrai propriétaire est disponible et correctement utilisé dans l'en-tête. Toute mise en demeure générée pour un autre propriétaire désigne donc le mauvais créancier dans son texte principal. Une mise en demeure au nom du mauvais bailleur peut être juridiquement contestable, met l'agence en porte-à-faux vis-à-vis de ses mandants, et divulgue le nom d'un client à des locataires d'autres clients.
La mise en demeure de niveau 3 code en dur le nom du bailleur « Société V FUND IMMO CORE » pour tous les propriétaires
Le corps du template de relance de niveau 3 code en dur l'identité du créancier : « vous restez à ce jour débiteurs de la somme de X euros à l'égard de votre bailleur la Société V FUND IMMO CORE », alors que le modèle expose ${model.ownerName} (correctement utilisé dans l'en-tête ligne 503). Chaque mise en demeure finale générée pour tout autre propriétaire/mandant nomme le mauvais bailleur dans son paragraphe opératoire — une mise en demeure juridiquement défectueuse (une identification erronée du créancier peut annuler la mise en demeure et embarrasse l'agence vis-à-vis de ses mandants ; cela divulgue aussi le nom d'un client aux locataires d'autres clients).
adb-charts/releases/templates/dunning-level3.ftlh:531adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:402
Contre-vérification — Re-dérivé : adb-charts/releases/templates/dunning-level3.ftlh:531 code en dur « votre bailleur la Société V FUND IMMO CORE » dans le paragraphe opératoire de la demande, alors que la ligne 503 du même template rend correctement ${model.ownerName}, et DunningServiceImpl.java:399-406 transmet dunning.getOwner().getName() dans le modèle pour chaque relance LEVEL_3 sans aucun filtre par propriétaire. « V FUND IMMO CORE » n'apparaît nulle part ailleurs sauf dans les fixtures de test ; les templates dunning-level1/2 ne nomment aucun bailleur dans le corps. Les templates sont servis depuis S3 (DocumentServiceImpl.getTemplate), mais cette copie du repo est la source déployable. La sévérité élevée est correcte : mise en demeure juridiquement défectueuse et fuite de nom entre clients, mais aucun impact au grand livre.
Exact, codé en dur.
Le module de relance considère comme candidat à la relance tout locataire dont le solde est positif, sans aucune notion d'échéance, de jours de retard ou de montant minimum. Comme l'appel de loyer débite le compte du locataire dès son émission, avant même la date d'exigibilité, tous les locataires apparaissent en impayé entre le quittancement et le paiement, y compris pour un reliquat d'un centime. Les gestionnaires ne peuvent pas distinguer un vrai retardataire d'un locataire simplement pas encore à l'échéance, sauf par des filtres manuels : charge de tri importante et risque de relances injustifiées.
L'éligibilité à la relance ignore les échéances et les seuils — les locataires sont signalés dès qu'un appel est passé
Un enregistrement de relance est créé/rafraîchi pour tout solde auxiliaire TENANT >= 0 sur un RentalContract ; il n'existe aucune notion de date d'échéance, de jours de retard ou de montant minimum dans le flux (le grep de dueDate/overdue/echeance dans les sources principales de adb-contracts et adb-accounting ne retourne rien). Comme les appels de loyer débitent 411100/RC à l'émission (avant que le loyer ne soit contractuellement exigible), chaque locataire présente un solde positif entre l'émission de l'appel et le paiement, de sorte que la liste de travail de relance ne peut distinguer un locataire réellement en retard d'un locataire dont le loyer n'est simplement pas encore échu ; un reliquat de 1 centime qualifie également. Les seuls filtres disponibles pour les utilisateurs sont des critères de requête ad hoc (plage de solde, previousDunningNoticeDate).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:225adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:164
Contre-vérification — Re-dérivé : CFR/RECURRING_RENT débite l'auxiliaire locataire 411100 avec postingDate par défaut à period.atDay(1) (RecurringRent.java:68) ; le solde auxiliaire courant (BalanceServiceImpl.auxiliaryBalance:99-120, sans qualification de date) déclenche le SQS « contractsAuxiliaryAccountBalanceUpdated » (DunningFunction.java:33-47), et processBalanceEvent ne filtre que amount>=0/TENANT/RentalContract (DunningServiceImpl.java:225-227). La liste de travail affiche tout solde>0 (DunningRepositoryImpl.java:163-168) ; DunningQuery et l'UI DunningNoticeFilters n'exposent aucun filtre d'échéance/retard/montant minimum ; les greps de dueDate/échéance/retard/échu/overdue/threshold dans les deux services et le code de relance de adb-ui ne retournent rien. Atténuation (maintient la sévérité moyenne) : les avis ne sont envoyés que par sélection manuelle du gestionnaire, ce qui dégrade la liste de travail plutôt que de relancer automatiquement les locataires.
Exact. Développé en v1 : cela n'a tout simplement pas été intégré à la première version.
Le courrier de relance ne totalise que les échéances non lettrées. Quand une indemnité GLI vient couvrir des loyers impayés, ces échéances sont lettrées côté locataire alors que sa dette globale subsiste (il doit désormais l'assureur par subrogation) : le locataire reste bien dans la liste des impayés, mais le courrier généré omet les sommes couvertes par la GLI. Résultat : une relance peut afficher zéro ou seulement les arriérés récents alors que la liste montre la dette complète — des courriers incohérents avec le solde, potentiellement inexploitables en recouvrement post-GLI.
Le total de la lettre de relance exclut en silence les arriérés apurés par des indemnités GLI, contredisant le solde affiché
Le modèle de lettre ne récupère que les écritures auxiliaires non lettrées (includeOnlyUnSettled=true) et totalise leurs montants ouverts. Une indemnité GLI lettre les écritures d'appel de loyer du locataire (lettrage contre le paiement GLI) sans passer aucun crédit à 411100/RC (insuranceIndemnitiesReceived ne passe que 512101 D / 405100 C), de sorte que le solde du locataire — calculé par BalanceCalculator comme débit moins crédit sur les écritures — reste positif et la relance reste listée (comme souhaité, puisque le locataire doit encore via subrogation), mais la relance/mise en demeure générée omet ces écritures lettrées : la lettre peut afficher 0 ou seulement des arriérés récents alors que la liste de relance affiche la dette complète. Les lettres de relance post-GLI sous-estiment la créance et contredisent le propre grand livre de l'agence.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/api/RentalContractAuxiliaryEntriesQuery.java:36adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/documents/DunningDocumentModel.java:45adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:90
Contre-vérification — Re-dérivé : le flux GLI passe 512101/405100 + 416100/800000 (PostingRuleFactory.java:84-140), sans jamais créditer le locataire 411, donc rcBalance (BalanceCalculator.java:36) conserve la dette complète et la relance reste listée. Pourtant l'écriture de crédit GLI est lettrée contre les débits d'appel de loyer du locataire — flux conçu ainsi selon adb-ui recieve-payment-modal proposeSettlement et CallForRent.java:103-113 branche GliToOwner. La requête de la lettre code en dur includeOnlyUnSettled=true (RentalContractAuxiliaryEntriesQuery.java:35 ; filtre dans AuxiliaryAccountAggregationOperations.java:64-69), donc DunningDocumentModel.entriesTotal omet les arriérés apurés par GLI ; aucun rcBalance dans le modèle de lettre ne le corrige. La lettre contredit la liste. Le cadrage juridique est discutable, mais l'incohérence est réelle ; moyen tient.
Exact : quand l'assurance GLI solde la dette d'un locataire, il s'agit techniquement d'une mise à jour d'un enregistrement existant, pas d'une création.
Le module de relance connaît le gestionnaire, le propriétaire et le locataire (y compris colocataires et conjoints), mais ignore totalement la caution : aucun champ, aucun destinataire, aucun suivi, alors que la plateforme sait par ailleurs gérer les contrats de cautionnement. Or le gestionnaire doit informer la caution des impayés — la loi impose notamment de signifier le commandement de payer à la caution sous 15 jours, faute de quoi elle n'est plus tenue des pénalités et intérêts. Le module ne peut ni produire ni tracer cette notification : risque de perdre une partie des recours contre la caution et d'affaiblir le recouvrement.
Aucune représentation ni notification de la caution nulle part dans le flux de relance
Le domaine de relance modélise Agent, Owner et Tenant (avec colocataires/conjoints via PersonFlatShare/PersonUnion) mais n'a aucune notion de garant : aucun champ caution/garant sur Dunning/DunningNotice/TenantEntity, aucun destinataire garant dans la génération ou la distribution des avis, bien que la plateforme modélise GuaranteeContract dans adb-model. Un administrateur de biens français doit notifier la caution des impayés du locataire — l'art. 24 I de la loi 89-462 exige que le commandement de payer soit signifié à la caution dans les 15 jours (sinon la caution n'est pas tenue des pénalités/intérêts), et informer la caution durant la phase amiable est de pratique courante pour préserver les droits de recouvrement. Le module ne peut produire ni suivre aucune communication au garant (le grep de « caution|guarant » dans le modèle/service de relance ne retourne que les champs de garantie financière de l'agence elle-même).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/dunning/Dunning.java:16adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/dunning/Tenant.java:23
Contre-vérification — Lacune re-dérivée : le package du modèle de relance (adb-contracts/.../model/dunning/) ne contient que Agent/Owner/Tenant/Dunning/DunningNotice ; DunningServiceImpl.sendDunningNotice et processNoticeSentEvent distribuent lettres/LRAR/e-mails aux locataires/colocataires uniquement ; le seul résultat « guarant » est le document FINANCIAL_GUARANTEE_PROPERTY_MANAGEMENT de l'agence (DunningDocumentServiceImpl.java:115). Les garants existent au niveau de la plateforme (adb-model GuaranteeContract, epic d'archive i-can-have-contract-guarantors.md) mais aucun fichier du monorepo ne lie garant et relance. Réserve : le commandement de l'art. 24 est un acte d'huissier hors de ce flux amiable (templates = relance/mise en demeure), mais l'art. 2303 Code civil exige tout de même d'informer la caution dès le premier incident de non-paiement, donc la lacune et la sévérité moyenne tiennent.
Les contrats de caution ont été implémentés après les avis de relance.
Le niveau de relance (relance simple, deuxième relance, mise en demeure) est entièrement décidé par l'écran de saisie : le système ne vérifie ni la progression logique des niveaux, ni un délai minimum depuis la dernière relance. Une manipulation erronée ou une requête rejouée peut donc envoyer un locataire directement en mise en demeure, ou le faire redescendre, sans aucune règle ni piste de contrôle. Par ailleurs, la date de relance est enregistrée en heure universelle et non dans le fuseau de l'agence, ce qui peut décaler d'un jour certaines dates. Risque limité mais réel de relances hors procédure et de dates incohérentes.
L'escalade des niveaux de relance repose entièrement sur la confiance client ; aucune règle serveur d'ordre, d'intervalle ou d'état
currentLevel (LEVEL_1/2/3) est ce que le frontend envoie dans sendDunningNotice : DunningMapper mappe la CatalogValue directement vers l'entité et le merge du repository la stocke ; il n'y a aucune vérification côté backend que LEVEL_2 suive LEVEL_1, aucun intervalle minimum depuis previousDunningNoticeDate, et les niveaux peuvent régresser ou sauter arbitrairement (le service ne lit ni ne compare jamais les niveaux, sauf pour choisir un template PDF). Une requête FE malformée ou rejouée peut donc faire passer un locataire directement au niveau 3 « mise en demeure », ou le réinitialiser, sans aucune règle d'audit. De plus previousDunningNoticeDate est inscrite avec LocalDate.now(UTC) au lieu du fuseau horaire de l'agence utilisé partout ailleurs (DunningServiceImpl utilise now(ctx.getAgencyTimeZone()) pour la date du document), de sorte que les avis envoyés avant ~01:00 heure de Paris sont enregistrés à la veille, faussant le filtrage par intervalle.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:161adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:419adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/DunningMapper.java:313
Contre-vérification — Affirmations de code re-dérivées : seul CatalogValueValidator (appartenance au catalogue) protège currentLevel ; DunningMapper.java:313-318 valueOf aveugle, DunningRepositoryImpl.java:389-390 le stocke, DunningServiceImpl.java:161-187 ne compare jamais les niveaux ni les intervalles ; :419 inscrit previousDunningNoticeDate en UTC vs le fuseau de l'agence à DunningServiceImpl.java:214/267, faussant le filtre de plage (repo :581-589). Cependant la sévérité est surestimée : le choix du niveau est intentionnellement modifiable par l'opérateur (adb-ui create-dunning-notice-table ng-select libre ; dunning.mapper.ts:56-61 ne fait que proposer le niveau suivant), le tableau versions préserve l'historique, et l'art. 24 loi 89-462 n'impose aucune séquence de relance ordonnée — le fondement juridique est mal énoncé. À rétrograder en faible.
Quand on règle une facture fournisseur rattachée à un mandat de gestion, la comptabilité enregistre deux fois la sortie de banque pour un seul paiement réel, et débite en plus le compte du propriétaire immédiatement — alors que cette imputation au propriétaire doit normalement se faire plus tard, au moment du rapprochement. Résultat : le compte banque en comptabilité s'écarte du relevé bancaire réel à chaque paiement fournisseur de ce type, et le propriétaire risque d'être débité deux fois de la même dépense. Cela fausse les rapprochements bancaires et les comptes rendus de gestion.
Le règlement fournisseur rattaché à un propriétaire crédite la banque deux fois et débite le propriétaire, en plus du SUPPLIER_TO_OWNER passé au lettrage
Lorsqu'un SUPPLIER_PAYMENT_ISSUED comporte un rentalDelegateContract dans son descriptor, la règle de comptabilisation passe QUATRE lignes au montant plein : D 401100/SUP, C 512101, D 466100/RDC, C 512101. Un seul mouvement de trésorerie de X est enregistré comme 2X de crédits bancaires, si bien que 512101 diverge du relevé bancaire réel à chaque règlement fournisseur rattaché à un propriétaire. Selon la spécification documentée (supplier-payment-issued.md : uniquement D 401100 / C 512101 ; la contrepartie propriétaire intervient plus tard via SUPPLIER_TO_OWNER 466100→800000 au lettrage, cf. cas INCOMING_SUPPLIER_INVOICE de postSettlementTransfer), le second crédit devrait au plus toucher 800000. Combiné au SupplierToOwner passé au lettrage (InvoiceAccountingEvent.postSettlement ; PostSettlementsProcessorImpl autorise SUPPLIER_PAYMENT_ISSUED comme type lettrant), le 466100 du propriétaire est débité deux fois pour une seule facture, minorant chaque reversement/CRG propriétaire ultérieur. Le transactionLineNumber est également dupliqué (1,2,2,3). Le test unitaire n'assert que le nombre d'écritures (4), pas les comptes, et fige donc le bug.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:421adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:76adb-accounting/src/test/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactoryTest.java:199adb-doc/docs/concepts/accounting/events/supplier-payment-issued.md:40
Contre-vérification — PostingRuleFactory.java:421-439 passe D fournisseur/C banque/D propriétaire/C banque tous au montant plein quand le descriptor porte un RDC (lignes 68-75) ; 512101 est donc crédité 2X pour un seul règlement, et les numéros de ligne sont dupliqués (1,2,2,3). La doc (supplier-payment-issued.md:40) ne prévoit que D 401100/C 512101, la contrepartie propriétaire passant via SUPPLIER_TO_OWNER au lettrage, lequel se déclenche toujours (PostSettlementsProcessorImpl.java:50 autorise SUPPLIER_PAYMENT_ISSUED ; InvoiceAccountingEvent.postSettlement ; postSettlementTransfer D propriétaire/C 800000, PostingRuleFactory.java:252-258) — le propriétaire est débité deux fois. Aucun contrôle d'équilibrage/déduplication n'existe (PostingRuleHandler ne rejette que zéro/négatif), et PostingRuleFactoryTest.java:199 n'assert que le nombre d'écritures 4.
Confirmé. supplierPaymentIssued() ajoute une paire D 466100 (propriétaire/RDC) / C 512101 (banque) au montant plein dès que le descripteur porte un rentalDelegateContract, en plus de la paire de base D 401100 / C 512101. Cela ressemble à une intention plus qu'à une coquille : probablement un reliquat des dernières mises à jour de l'Event Matrix par Pierre, où une jambe propriétaire avait été ajoutée à cet événement avant que la spec ne fixe de la comptabiliser plus tard via SUPPLIER_TO_OWNER au lettrage. Correctif (petit) : supprimer la branche débit propriétaire / crédit banque dans getLedgerEntryShortArrayList (PostingRuleFactory.java:421-440) ; la duplication des numéros de ligne se résout automatiquement. Base de données : vérifiée, aucune correction de données nécessaire.
Lorsqu'un compte rendu de gestion (CRG) est établi, il marque comme traitées toutes les écritures jusqu'à la date de fin de période, y compris celles du jour de clôture lui-même et celles antidatées avant le début de la période (par exemple une correction saisie après le CRG précédent). Or le rapport imprimé, lui, ne reprend que les écritures strictement comprises dans la période. Ces écritures marquées mais non reprises disparaissent définitivement : elles ne figurent ni sur ce CRG ni sur le suivant. Le propriétaire reçoit donc un compte rendu incomplet, sans aucun signal d'alerte.
La fenêtre de rattachement des écritures au CRG (postingDate <= upFrom, cumulatif) ne correspond pas à la fenêtre du rapport ([downTo, upFrom)) : les écritures du jour-limite et antidatées sont consommées mais jamais restituées
La proposition/traitement d'une période CRG rattache TOUTES les écritures auxiliaires non taguées ayant postingDate <= upFrom (sans borne basse). Le rapport PDF, lui, sélectionne les écritures telles que downTo <= postingDate < upFrom. Deux catégories d'écritures sont donc rattachées à la période mais exclues de son rapport : (1) les écritures passées exactement à la date de fin de période upFrom (lte vs lt, décalage d'une unité — ce jour appartient à la période puisque la suivante démarre à upFrom+1) ; (2) les écritures antidatées dont le postingDate est antérieur à downTo (ex. une correction passée après le traitement du CRG précédent). Aucune n'apparaît dans le rapport de cette période, et la période suivante ne peut les reprendre (déjà taguées). Pire : comme la requête du rapport ne filtre que sur crgDescriptor-existe + état + plage de dates (pas sur crgDescriptor.upFrom), régénérer le PDF d'une ANCIENNE période y inclut silencieusement l'écriture antidatée, si bien qu'un compte rendu de gestion déjà envoyé change rétroactivement.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CRGPeriodCustomRepositoryImpl.java:201adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CRGPeriodCustomRepositoryImpl.java:310adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:244
Contre-vérification — Re-dérivé du code : le rattachement utilise postingDate lte(upFrom) sans borne downTo (CRGPeriodCustomRepositoryImpl.java:201-202) alors que le rapport utilise gte(downTo).lt(upFrom) (lignes 310-311), et la période suivante démarre à upFrom+1 (CRGServiceImpl.java:244) ; les écritures du jour upFrom et antidatées sont donc rattachées mais jamais détaillées. Aucun garde-fou ne bloque les écritures antidatées, et la requête du rapport omet le filtre crgDescriptor.upFrom pourtant présent dans son propre commentaire de référence (ligne 215), si bien que régénérer le PDF d'une ancienne période PROCESSED (CRGController.java:115) capte les écritures antidatées taguées ensuite. Gravité élevée (non critique) : les montants remontent tout de même dans le "solde antérieur" de la période suivante (CRGPeriodRecapFactoryImpl.java:204-276), donc pas de perte monétaire, seulement des défauts de détail/stabilité.
Cela paraît valide. La requête de marquage et la requête du rapport doivent utiliser les mêmes bornes de période — sinon des écritures peuvent être marquées comme consommées par un CRG sans figurer dans le rapport généré. La fenêtre de marquage doit s'aligner sur celle du rapport : downTo <= postingDate < upFrom.
La colonne « reversé au propriétaire » du compte rendu de gestion ne compte que les encaissements locataires classiques, avec deux erreurs connues (un correctif était même annoté dans le système mais jamais réalisé). D'une part, quand le dépôt de garantie est conservé par l'agence, le CRG l'affiche quand même comme reversé au propriétaire — soit environ un mois de loyer surestimé. D'autre part, quand un loyer est couvert par une indemnité GLI, le propriétaire est bien crédité mais le CRG ne le montre pas. Les comptes rendus envoyés aux propriétaires sont donc faux dans ces deux situations.
La colonne « dû au propriétaire » du CRG ne compte que les lettrages TENANT_PAYMENT_RECEIVED : les dépôts de garantie conservés par l'agence sont indiqués comme reversés au propriétaire, les transferts financés par la GLI sont omis (FIXME reconnu)
Le dueToOwner par écriture du rapport somme les lettrages dont settlingAccountingEventType == TENANT_PAYMENT_RECEIVED, avec un `// FIXME: add TENANT_TO_OWNER filter when available` explicite. Conséquences : (1) CALL_FOR_DEPOSIT figure dans MISCELLANEOUS_EVENT_TYPES et reste sur le rapport ; quand le locataire verse le dépôt de garantie et que rentDepositBeneficiary=MANDATE, le grand livre l'oriente correctement vers l'agence (TenantToAgency 165100→165200, le propriétaire n'est jamais crédité — CallForDeposit.postSettlement), mais la colonne dueToOwner du CRG inclut tout de même ce lettrage, surévaluant le « reversé au propriétaire » typiquement d'un mois de loyer. (2) Le loyer réglé par indemnités GLI génère un GliToOwner (le propriétaire EST crédité), mais ces lettrages sont exclus de dueToOwner, sous-évaluant le reversement affiché. Le PDF restitue ces valeurs directement (CRG.ftlh dueToOwner / dueToOwnerWithPaidInvoicesTotal), si bien que le compte rendu de gestion officiel diverge du grand livre 466100.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:241adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:167adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForDeposit.java:99adb-charts/releases/templates/CRG.ftlh:630
Contre-vérification — Re-dérivé : dueToOwner ne somme que les lettrages TENANT_PAYMENT_RECEIVED (CRGRentalContractEntriesProjection.java:241-245, FIXME reconnu). CALL_FOR_DEPOSIT est conservé sur le CRG (MISCELLANEOUS_EVENT_TYPES ligne 167 ; CRGPeriodRecapFactoryImpl.java:286 le maintient), et aucun filtre du pipeline ne l'exclut (CRGPeriodCustomRepositoryImpl.defaultFilter:440). Avec rentDepositBeneficiary=MANDATE, CallForDeposit.java:99-108 passe TenantToAgency (165100→165200, propriétaire jamais crédité), mais le lettrage du dépôt compte tout de même comme dû au propriétaire — surévaluant le « Total du PROPRIETAIRE » (CRG.ftlh:630/647/691). À l'inverse, le loyer réglé par GLI (settlingAccountingEventType=GLI_INDEMNITIES_RECEIVED, InsuranceIndemnitiesReceived.java:44) est exclu bien que GliToOwner crédite 466100 (CallForRent.java:105). Défaut de reporting uniquement (grand livre correct), mais anomalie significative sur le relevé officiel du propriétaire : élevé.
Implémentation incomplète, valide. Le FIXME existant montre que la prise en charge de TENANT_TO_OWNER et des autres événements alimentant le propriétaire était prévue mais n'a pas été terminée ; de ce fait, les reversements financés par la GLI et d'autres versements éligibles peuvent être omis du CRG. Le calcul doit inclure tous les types de règlement/événement qui contribuent réellement au reversement propriétaire.
Les honoraires de gestion calculés en pourcentage prennent pour base tout ce qui est facturé au locataire sur la période, dépôt de garantie compris. Le premier mois, où le dépôt (souvent un mois de loyer) s'ajoute au loyer et aux charges, les honoraires sont donc à peu près doublés — et le propriétaire paie des honoraires sur une somme qui n'est pas un revenu, et qui, si le dépôt est conservé par l'agence, ne lui parvient même pas. De plus, la base est ce qui a été appelé, pas ce qui a été réellement encaissé : des honoraires sont facturés même sur des loyers impayés.
Les honoraires de gestion en pourcentage sont calculés sur des montants appelés incluant le dépôt de garantie (CALL_FOR_DEPOSIT dans l'assiette des honoraires)
Les factures d'honoraires propriétaire déclenchées par CFR calculent leur assiette en sommant les écritures débit-moins-crédit de la période sur FEE_BASIS_EVENTS_CFR_AWARE, qui inclut explicitement CALL_FOR_DEPOSIT. Un MANAGEMENT_FEES_WITHOUT_VAT en pourcentage (branche pourcentage de RentalFeesAmount, +20% de TVA) est ensuite appliqué à ce total. Lors de la première période de loyer, où le dépôt de garantie (typiquement un mois de loyer) est appelé en même temps que le loyer et les charges, les honoraires de gestion de l'agence sont à peu près doublés et le propriétaire est facturé sur une somme qui n'est pas un revenu (et qui, sous bénéficiaire MANDATE, ne lui parvient même jamais). De plus, l'assiette est ce qui a été APPELÉ, non ce qui a été encaissé : les honoraires configurés sur le déclencheur CFR sont facturés sur un loyer que le propriétaire peut ne jamais percevoir (les honoraires sont d'usage un % des encaissements).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/CallForRentFeesInvoiceCreationStrategy.java:31adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:100adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/AbstractFeesInvoiceCreationStrategy.java:126
Contre-vérification — Re-dérivé : CALL_FOR_DEPOSIT figure dans FEE_BASIS_EVENTS_CFR_AWARE (CallForRentFeesInvoiceCreationStrategy.java:31-36). La requête d'assiette filtre sur les comptes auxiliaires du contrat de location (DomainObjectEntryReactiveRepository.java:158-159), si bien que seul le débit 411 du dépôt correspond (RentPostingRuleFactory.java:511-533) — le crédit 165100 est exclu, gonflant débit−crédit du montant plein du dépôt. CallForDeposit porte descriptor.callForRentPeriod (DescriptorHandler.java:207-215), correspondant au CallForRent secondaire de la période initiale (InitialRent.java:132-141) qui route vers cette stratégie (InvoiceCreationStrategyFactoryImpl.java:38). MANAGEMENT_FEES en pourcentage + déclencheur CFR + 20% de TVA l'appliquent (RentalFeesAmount.java:101-105). Le chemin de lettrage exclut les dépôts (SettlementFeesInvoiceCreationStrategy.java:34), confirmant l'absence d'intention. Limité à la première période, dépendant de la configuration : moyen.
Logique métier attendue, pas un bug. L'écriture de dépôt de garantie est incluse intentionnellement dans la base des honoraires, conformément à la spécification.
La création et le traitement d'un compte rendu de gestion n'imposent aucun contrôle d'enchaînement : on peut créer une période qui chevauche la précédente, laisse un trou, ou arrive dans le désordre — et en la traitant, absorber des écritures qui appartiennent à d'autres périodes. Pire, un CRG déjà finalisé et envoyé au propriétaire peut être supprimé sans aucune trace : ses écritures redeviennent libres et peuvent être reprises dans un nouveau compte rendu, sans annulation, sans historique, sans piste d'audit. C'est un risque d'erreur de gestion et un problème de traçabilité vis-à-vis des propriétaires.
Cycle de vie des périodes CRG non protégé : les POST propose/process ignorent le contrôle de dernière-période, aucune validation de continuité/chevauchement, et un CRG PROCESSED peut être supprimé
Seuls les endpoints PUT (mise à jour) et DELETE appellent validateThatCrgIsLastOne ; POST /propose et POST /process acceptent n'importe quel upFrom pour n'importe quel propriétaire/RDC sans vérifier qu'il suit la période précédente, qu'il ne la chevauche pas, ou que downTo == previousUpFrom+1 (le corps fourni par le client porte downTo). Un utilisateur peut traiter une période hors séquence dont le rattachement cumulatif (postingDate <= upFrom) engloutit des écritures appartenant à d'autres périodes. deletePeriod ne vérifie que « est la dernière », pas l'état : le delete Mongo dé-set crgDescriptor sans filtre CRGPeriodState, si bien qu'un CRG PROCESSED (finalisé, envoyé au propriétaire) peut être effacé sans trace, détaguant ses écritures pour une re-proposition silencieuse — aucune protection par extourne, versionnage ou audit. validateThatCrgIsLastOne lui-même ne compare que l'égalité de upFrom avec le maximum, l'état PROCESSED vs PROPOSED n'étant jamais pris en compte nulle part.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java:123adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CRGPeriodCustomRepositoryImpl.java:173adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:155
Contre-vérification — CRGController.java:123-142 : les POST ignorent validateThatCrgIsLastOne (seuls PUT :153/:169 et DELETE :181 l'appellent). doSave (CRGServiceImpl.java:155-176) n'a aucun contrôle d'ordre/continuité ; downTo est fourni par le client. forOwnerAndRdcByPeriodAndStates (CRGPeriodCustomRepositoryImpl.java:195-207) matche postingDate<=upFrom sans filtre crgDescriptor.upFrom, si bien que traiter une période ultérieure co-proposée engloutit les écritures proposées antérieures hors de tous les rapports. validateThatCrgIsLastOne (:140-147) ne vérifie que l'égalité de upFrom, jamais l'état ; le delete (:173-179) dé-set crgDescriptor sans filtre d'état ni audit. L'UI de prod propose même la suppression du dernier CRG VALIDATED (JournalsService.ts:1240-1243). Gravité moyenne appropriée.
Intentionnel. L'utilisateur crée les périodes de CRG et peut choisir les dates voulues, y compris des périodes qui ne s'enchaînent pas automatiquement au rapport précédent. Un CRG traité n'est qu'un rapport généré : le supprimer ne modifie ni les écritures comptables ni aucun autre état du système, le client peut donc le supprimer si nécessaire.
Quand on saisit un virement vers un propriétaire, aucun contrôle ne vérifie le solde réellement disponible sur son compte : le montant saisi est accepté tel quel. On peut donc lui reverser un loyer qui n'a jamais été encaissé, ou faire un versement alors que sa position réelle (dépôt de garantie, GLI) est plus basse. Son compte passe alors silencieusement en négatif, ce qui signifie que l'agence avance de l'argent — et pour un mandataire avec un compte bancaire commun, c'est l'argent des autres propriétaires qui est utilisé. Seule une vigilance manuelle protège contre cette situation.
OWNER_PAYMENT_ISSUED ne comporte aucun garde-fou contre le dépassement du solde 466100 du propriétaire (l'agence avance les fonds)
Un paiement sortant ciblant un RentalDelegateContract est mappé sur OWNER_PAYMENT_ISSUED et comptabilisé D 466100/RDC, C 512101 pour tout montant saisi. Ni les stratégies de création de paiement, ni la machine à états de comptabilisation (BookPaymentAction), ni la règle de comptabilisation ne consultent le solde auxiliaire du propriétaire (BalanceCalculator ne sert qu'à l'affichage/agrégation). Rien n'empêche de comptabiliser un reversement supérieur aux crédits accumulés via TENANT_TO_OWNER — c.-à-d. payer au propriétaire un loyer jamais encaissé, ou reverser alors que la position dépôt/GLI rend le solde réel inférieur. Le compte 466100 passe silencieusement en débit. Comme un mandataire détenant un seul compte bancaire mutualisé utiliserait alors nécessairement les fonds d'autres mandants, on attend au minimum un avertissement/une validation sur le solde encaissé ; il n'en existe aucun dans adb-accounting (grep : aucun contrôle balance/negative/insufficient dans les flux de paiement).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/PaymentHelper.java:20adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:50adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/payment/actions/BookPaymentAction.java:29
Contre-vérification — PaymentHelper.java:20 → Payment.java:243 → PostingRuleFactory.java:50-54 comptabilisent D466100/C512101 pour tout montant ; la seule validation du flux de comptabilisation est un contrôle d'état (AbstractProcessPaymentAction.java:34). Aucun contrôle de solde/fonds insuffisants n'existe dans adb-accounting (greps vides ; BalanceCalculator uniquement utilisé dans le reporting AbstractLedgerAccount). L'UI (accounting-payment-form.component.ts:241-244, .html:97-127) affiche le solde propriétaire et le solde après paiement mais ne valide le montant qu'avec required/min=0.01 — informatif, non bloquant — et PaymentController.createPayment accepte tout montant. Le dépassement de 466100 depuis un compte mandant mutualisé est re-dérivable ; gravité moyenne maintenue (lacune de garde-fou, l'UI affiche le solde).
Comportement comptable autorisé plutôt qu'un bug. Si l'agence paie plus que le solde propriétaire courant, le compte 466100 devient négatif : cela représente une avance financée par l'agence, et la différence reste une dette à recouvrer ultérieurement. Un avertissement dans l'UI pourrait être utile, mais le backend n'a pas nécessairement à bloquer le paiement.
Pour un compte rendu de gestion mensuel calé sur le 29, 30 ou 31 du mois, la période suivante proposée automatiquement se décale après un mois court : après février, le système propose des périodes qui commencent et finissent en milieu de mois, et le décalage devient permanent. La date de début n'étant pas modifiable, le gestionnaire doit corriger manuellement la date de fin à chaque période ; s'il accepte la proposition, tous les CRG suivants sont désalignés des périodes de loyer et un même mois de loyer se retrouve à cheval sur deux comptes rendus.
La période CRG suivante proposée dérive après les mois courts (écrêtage de plusMonths sur les ancrages de fin de mois)
getNextPeriod propose nextDownTo = previousUpFrom+1 jour et nextUpFrom = previousUpFrom+1 mois. Pour un CRG mensuel ancré au 29/30/31, l'écrêtage de java.time fait dériver la suggestion de façon permanente : 31 janv. → 28 févr. (correct), puis 28 févr. → suggère 1er–28 mars, puis 29 mars–28 avr., etc. Chaque période ultérieure commence/finit en milieu de mois au lieu d'être alignée sur le mois. downTo est verrouillé (disableEditing), si bien que l'utilisateur ne peut réparer qu'en repoussant manuellement upFrom à la fin du mois à chaque fois ; s'il accepte la valeur par défaut, toutes les fenêtres CRG suivantes sont désalignées des périodes de loyer (les lignes groupées par YearMonth chevauchent alors deux CRG).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:241
Contre-vérification — CRGServiceImpl.java:241-249 propose nextUpFrom = previousUpFrom.plusMonths(1) ; l'écrêtage de java.time (31 janv.→28 févr.→28 mars) fait dériver de façon permanente les suggestions de CRG ancrées en fin de mois. Sémantique vérifiée : upFrom=fin de période, downTo=début de période (journals-mapper.ts:317-321 ; CRGServiceImplTest.java:108-125). getNextPeriod s'enchaîne sur la dernière période (repo $sort downTo:-1) et aucune normalisation de fin de mois n'existe dans adb-accounting ni adb-ui (crg-form.component.ts:111-118 reprend la réponse verbatim ; FROM verrouillé, TO éditable). La gravité faible est correcte : la date de fin reste éditable par l'utilisateur et les périodes restent contiguës (downTo=prevUpFrom+1j), donc aucune écriture n'est perdue ou comptée deux fois — impact d'alignement/UX uniquement.
Problème de calcul de date, valide. Appeler plusMonths(1) depuis une date de fin de mois peut déplacer définitivement l'ancre suggérée au 28, 29 ou 30. La logique de suggestion doit préserver la sémantique de fin de mois dès lors que l'upFrom précédent était le dernier jour de son mois.
Chaque fois que l'agence paie un fournisseur pour le compte d'un propriétaire sous mandat, la comptabilité enregistre deux fois la sortie d'argent en banque alors qu'un seul virement est réellement parti. Le solde de banque en comptabilité s'écarte donc du relevé bancaire à chaque règlement de ce type, ce qui rend les rapprochements bancaires faux et fastidieux. De plus, le propriétaire est débité une première fois au moment du paiement puis une seconde fois lors de son arrêté de compte, donc son CRG lui impute la dépense en double. Le fichier FEC produit comporte aussi des numéros de ligne en doublon, une anomalie détectable en cas de contrôle fiscal.
SUPPLIER_PAYMENT_ISSUED crédite deux fois le compte bancaire (et duplique les numéros de ligne FEC) lorsqu'un mandat de gestion propriétaire est rattaché
Lorsque le descriptor de l'événement de paiement porte un rentalDelegateContract, supplierPaymentIssued construit 4 écritures : Débit 401100 fournisseur / Crédit 512101 banque, puis Débit 466100 propriétaire / Crédit 512101 banque À NOUVEAU — 512101 est crédité 2x la trésorerie réellement décaissée, si bien que le grand livre bancaire diverge du relevé bancaire à chaque paiement fournisseur rattaché à un propriétaire. Par symétrie avec le SUPPLIER_PAYMENT_RECEIVED documenté sur 4 lignes (Débit 512101+800000 / Crédit 401100+466100), le second crédit devrait viser 800000, et la page de spécification de SUPPLIER_PAYMENT_ISSUED ne documente que 2 lignes (401100 -> 512101). Le propriétaire est de plus débité ici ET à nouveau par SUPPLIER_TO_OWNER au lettrage. Les numéros de ligne sortent en 1,2,2,3 (le 2 est dupliqué). Le test unitaire fige 4 écritures sans vérifier les comptes.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:421adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:76adb-doc/docs/concepts/accounting/events/supplier-payment-issued.md:43adb-accounting/src/test/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactoryTest.java:199
Contre-vérification — Re-dérivé depuis le code : PostingRuleFactory.java:421-439 ajoute Débit 466100 propriétaire plus un second Crédit sur le même compte bancaire 512101 (creditAccount, ligne 76) au montant plein lorsque le descriptor porte un rentalDelegateContract — la banque est créditée 2x pour un seul décaissement ; les numéros de ligne émettent 1,2,2,3. La spéc (supplier-payment-issued.md:41-44) ne documente que 2 lignes ; le miroir RECEIVED documenté sur 4 lignes utilise 800000 (analytique, LedgerAccountShort.java:385-391) comme contrepartie, cohérent avec postSettlementTransfer (PostingRuleFactory.java:251-256). SupplierToOwner (InvoiceAccountingEvent.java:55-73) se déclenche toujours inconditionnellement après lettrage, double-débitant 466100. Le test (PostingRuleFactoryTest.java:199) n'assertionne que le nombre d'écritures. Aucun garde-fou ne réfute le constat.
Confirmé. supplierPaymentIssued() ajoute une paire D 466100 (propriétaire/RDC) / C 512101 (banque) au montant plein dès que le descripteur porte un rentalDelegateContract, en plus de la paire de base D 401100 / C 512101. Cela ressemble à une intention plus qu'à une coquille : probablement un reliquat des dernières mises à jour de l'Event Matrix par Pierre, où une jambe propriétaire avait été ajoutée à cet événement avant que la spec ne fixe de la comptabiliser plus tard via SUPPLIER_TO_OWNER au lettrage. Correctif (petit) : supprimer la branche débit propriétaire / crédit banque dans getLedgerEntryShortArrayList (PostingRuleFactory.java:421-440) ; la duplication des numéros de ligne se résout automatiquement. Base de données : vérifiée, aucune correction de données nécessaire.
Quand un fournisseur rembourse l'agence (avoir de copropriété, double paiement), la règle prévue est de recréditer le propriétaire qui avait financé la dépense. En pratique, seule la moitié des écritures prévues est passée : l'argent rentre en banque et solde le fournisseur, mais le compte du propriétaire n'est jamais recrédité, et aucun mécanisme de rattrapage ne le fait plus tard. Le remboursement reste donc parqué côté agence et le propriétaire est durablement lésé du montant remboursé, sans qu'aucune alerte ne le signale.
SUPPLIER_PAYMENT_RECEIVED ne comptabilise que 2 des 4 lignes documentées — les remboursements fournisseurs n'atteignent jamais le grand livre propriétaire
La spéc (miroir de la feuille Event Matrix) définit SUPPLIER_PAYMENT_RECEIVED comme Débit 512101 + Débit 800000 / Crédit 401100 + Crédit 466100, afin qu'un remboursement fournisseur (avoir copro, double paiement) soit recrédité au propriétaire. Le code ne comptabilise que Débit 512101 / Crédit 401100. Il n'existe aucun repli au lettrage non plus : SUPPLIER_PAYMENT_RECEIVED n'est ni dans POST_SETTLEMENT_ALLOWED_EVENTS ni dans ALLOWED_CREDIT_EVENT_TYPE, donc aucun transfert SUPPLIER_TO_OWNER inverse ne se déclenche jamais. Un remboursement de charges financées par le propriétaire reste parqué sur le compte fournisseur / les fonds de l'agence ; le propriétaire est durablement lésé (SUPPLIER_TO_OWNER, quand il se déclenche, ne fait que débiter les propriétaires, jamais les créditer).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:56adb-doc/docs/concepts/accounting/events/supplier-payment-received.md:43adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/AccountingFunctions.java:51
Contre-vérification — PostingRuleFactory.java:56-63 ne comptabilise que Débit 512101/Crédit 401100 (figé par test à 2 écritures, PostingRuleFactoryTest.java:164-183), alors que la spéc dérivée de la feuille (_events.json:33, supplier-payment-received.md:43) exige Débit 512101+800000/Crédit 401100+466100. Aucun repli n'existe : SUPPLIER_PAYMENT_RECEIVED est absent de POST_SETTLEMENT_ALLOWED_EVENTS (AccountingFunctions.java:51-62) et de ALLOWED_CREDIT_EVENT_TYPE (PostSettlementsProcessorImpl.java:49-53) ; Payment utilise le postSettlement par défaut vide (AbstractAccountingEvent.java:347). Asymétrie prouvée dans le code : supplierPaymentIssued (lignes 65-82) débite le propriétaire 466100 quand un mandat de délégation existe, mais les remboursements ne le recréditent jamais. Seuls des contournements manuels (MANUAL_ENTRY, contre-passation d'événement) existent ; la sévérité élevée est maintenue.
Problème valide. La spécification définit quatre lignes (Débit 512101, Débit 800000, Crédit 401100, Crédit 466100) ; l'implémentation n'en passe qu'une partie, si bien que le remboursement fournisseur n'atteint pas le compte du propriétaire comme prévu. Le code concerné ne semble pas avoir été mis à jour depuis 2021 : vraisemblablement un ancien décalage entre implémentation et spécification dû à un défaut de communication.
Tous les montants sont arrondis au centime en tranchant vers le bas, alors que l'usage commercial (et la plupart des documents contractuels) arrondit au plus proche vers le haut en cas d'égalité. Sur les loyers proratisés notamment, cela crée un petit écart d'un centime, toujours dans le même sens, répété sur des milliers de quittancements, et des différences possibles avec les montants figurant sur les documents remis aux locataires. Par ailleurs, les montants des écritures sont enregistrés dans un format informatique imprécis pour la monnaie, qui peut dans de rares cas décaler un montant d'un centime. L'impact unitaire est faible mais il est systématique.
Chaque montant monétaire est arrondi en HALF_DOWN à la construction et les montants des règles de comptabilisation sont persistés en Double binaire
Le constructeur de Money ré-échelonne chaque montant à 2 décimales avec RoundingMode.HALF_DOWN, si bien que toutes les limites au demi-centime (ex. loyers proratisés : loyer mensuel divisé à l'échelle 4 puis re-multiplié) sont systématiquement arrondies vers le bas au lieu du HALF_UP commercial standard — un biais faible mais directionnel sur des milliers d'appels de loyer, et un écart avec tout document contractuel arrondi en half-up. Séparément, les écritures de grand livre des règles de comptabilisation sont persistées via MoneyMapper.toDouble dans des champs `Double amount` (PostingRuleEntity/LedgerEntryShortEntity), stockant la monnaie en virgule flottante binaire ; un aller-retour BigDecimal->double->BigDecimal peut décaler les montants sur des valeurs malchanceuses, et aucun convertisseur Decimal128 n'est enregistré dans MongoConfig.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/Money.java:96adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/mapper/MoneyMapper.java:19adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/entity/value/LedgerEntryShortEntity.java:12
Contre-vérification — Money.java:39,97 arrondit bien tout montant construit en HALF_DOWN, non testé et incohérent avec le HALF_UP utilisé dans Money.divide (ligne 83), Quantity (114/162/178) et CallForRentUtil (40/94) ; cas au demi-centime atteignable : RentalFeesAmount.java:70-73 (12,25 x 10% TVA = 1,225 -> 1,22 et non 1,23). Mais l'exemple de proratisation est faux (RentalContract.java:846-871 utilise Quantity, HALF_UP), et la moitié « persistance en Double » est réfutée : PostingRuleEntity/LedgerEntryShortEntity sont du code mort (référencés seulement par ValueEntitiesSmokeTest), et les allers-retours BigDecimal échelle 2 ->double->BigDecimal.valueOf sont exacts en-dessous de ~10^13 EUR. Sévérité abaissée à basse.
HALF_DOWN est intentionnel : Pierre l'a explicitement défini comme la règle d'arrondi de notre comptabilité — ce point n'est pas un bug. La préoccupation sur le Double binaire est techniquement valable en général, mais les méthodes de conversion concernées ne semblent pas utilisées dans le flux actuel (ajoutées en 2020) : à traiter comme du code hérité/déprécié à supprimer ou remplacer, pas comme un risque de production actif.
La comptabilité dispose d'un seul garde-fou vérifiant que chaque écriture est équilibrée (total débits = total crédits) avant enregistrement. Or ce contrôle est justement débranché pour la famille d'opérations qui reverse l'argent aux propriétaires lors des arrêtés de compte. Aujourd'hui aucune erreur avérée n'en découle, mais si un défaut apparaissait un jour dans ces reversements, des écritures déséquilibrées s'enregistreraient en silence et fausseraient les comptes propriétaires sans aucun message d'erreur, alors que la même anomalie ailleurs serait bloquée immédiatement.
Le seul contrôle d'équilibre débit=crédit est ignoré pour tous les événements TransferBalanceSheet (TENANT/SUPPLIER/GLI_TO_OWNER, TENANT_TO_AGENCY)
EntryHandler.handleResultingTransaction est le seul endroit où sum(débits)==sum(crédits) est imposé avant qu'une transaction ne soit persistée, mais il exempte explicitement chaque sous-classe de TransferBalanceSheet — précisément la famille des transferts de lettrage qui déplace l'argent vers les comptes propriétaires et le miroir 800000. Tout défaut dans postSettlementTransfer/tenantToOwnerPayment produisant un jeu d'écritures déséquilibré serait persisté silencieusement et corromprait les grands livres propriétaires sans erreur, alors que le même défaut dans toute autre famille d'événements déclencherait NON_ZERO_BALANCE.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/EntryHandler.java:132adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/TenantToOwner.java:19
Contre-vérification — EntryHandler.java:134-137 contourne bien l'unique contrôle sum(débit)==sum(crédit) pour chaque sous-classe de TransferBalanceSheet (TenantToOwner.java:19, TenantToAgency, GliToOwner, SupplierToOwner) ; grep confirme que NON_ZERO_BALANCE n'est imposé nulle part ailleurs. Aucune justification à l'exemption : toutes les règles de comptabilisation de transfert (PostingRuleFactory.postSettlementTransfer→transfer(), lignes 342-376 ; tenantToOwnerPayment, lignes 186-224) émettent un DÉBIT + un CRÉDIT du même montant, donc le contrôle passerait toujours — la dérogation ne fait que retirer une protection. L'impact est latent (nécessite une future régression du constructeur de règles pour corrompre les grands livres propriétaire/800000), donc la sévérité « basse » est correcte ; c'est une lacune de défense en profondeur, pas une corruption active.
Doublon d'un constat déjà revu ; je ne le considère pas comme critique pour les flux actuels. Imposer le contrôle débit == crédit sur ces événements resterait toutefois un durcissement défensif utile, pour détecter plus tôt d'éventuelles erreurs futures de règles de comptabilisation.
La documentation comptable de référence présente un schéma d'écritures pour les honoraires facturés aux propriétaires qui, en réalité, n'est plus jamais utilisé : les honoraires passent aujourd'hui par un autre circuit (facturation agence puis reversement à l'arrêté de compte). Aucune écriture n'est fausse aujourd'hui, mais toute personne qui modifierait la comptabilité des honoraires en se fiant à la documentation partirait sur une base erronée. Un type d'opération vide subsiste aussi dans le système : s'il était utilisé par erreur, il provoquerait une panne tardive et difficile à diagnostiquer plutôt qu'un rejet clair.
L'événement OWNER_FEES et sa règle de comptabilisation à 4 lignes sont du code mort alors que la doc les présente comme actifs ; NULL_EVENT_TYPE est totalement non référencé
RentPostingRuleFactory.ownerFees (Débit 622xxx / Crédit 800000 / Débit 466100 / Crédit 402100) n'a aucun site d'appel, et la classe d'événement OwnerFees n'est jamais construite nulle part dans le monorepo (seulement hydratée depuis des entités Mongo héritées par AbstractAccountingEventMapper). Les honoraires propriétaires transitent en réalité par les stratégies OUTGOING_AGENCY_INVOICE (postes 622xxx via InvoiceHelper) plus SUPPLIER_TO_OWNER au lettrage. La page d'événement de la doc et la matrice décrivent toujours OWNER_FEES comme le porteur générique, ce qui induira en erreur tout mainteneur modifiant les comptabilisations d'honoraires. NULL_EVENT_TYPE n'existe que dans l'enum sans aucun chemin de traitement ; un événement le portant échouerait au fond de la chaîne de handlers plutôt que d'être rejeté d'emblée.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/RentPostingRuleFactory.java:695adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/OwnerFees.java:20adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java:80
Contre-vérification — Toutes les affirmations re-dérivées. RentPostingRuleFactory.ownerFees (RentPostingRuleFactory.java:695-749, Débit 622xxx/Crédit 800000/Débit 466100/Crédit 402100 selon LedgerAccountShort.java:128-387) n'a aucun appelant — les seuls appels ownerFees( résolvent vers InvoiceHelper.ownerFees (AbstractFeesInvoiceCreationStrategy.java:240). OwnerFees n'est qu'hydraté depuis des entités Mongo héritées (AbstractAccountingEventMapper.java:251) et absent des @JsonSubTypes d'adb-accounting (AbstractAccountingEvent.java:33-44) et des producteurs adb-contracts. La seule référence en source principale de NULL_EVENT_TYPE est AccountingEventType.java:80 ; sync-accounting-events.js:42 le liste déjà en whitelist comme placeholder. owner-fees.md affiche le statut « Développé » avec des comptabilisations d'apparence active et des sites d'appel morts, bien qu'il mentionne la spécialisation RENTING_*. Sévérité basse correcte : aucun impact monétaire à l'exécution.
OWNER_FEES relève de l'ancien modèle d'honoraires. Aujourd'hui, honoraires propriétaire et locataire sont générés et comptabilisés via des factures (une facture peut porter plusieurs honoraires, chacun avec sa propre écriture). L'absence de sites d'appel actifs pour OwnerFees et sa règle de comptabilisation est donc attendue : la documentation devrait être mise à jour et l'ancien code marqué déprécié ou supprimé. NULL_EVENT_TYPE n'a jamais servi dans un cas réel — également inutilisé/déprécié.
Quand un locataire régularise des sommes déjà indemnisées par l'assurance loyers impayés (GLI), l'agence rembourse l'assureur, et la facture correspondante est systématiquement découpée en hors-taxes plus TVA à 20 %. Or les opérations d'assurance ne sont pas soumises à la TVA en France, et ce remboursement d'indemnité n'a rien d'une prestation taxable. Le montant total payé à l'assureur reste juste, mais tout état ou déclaration de TVA établi à partir du détail des factures affiche une TVA déductible qui n'existe pas — une exposition en cas de contrôle fiscal.
La facture de remboursement GLI code en dur une TVA à 20% sur un remboursement d'indemnité d'assurance
InsurancePaybackInvoiceCreationStrategy construit la facture fournisseur qui rembourse l'assureur GLI (compte de charge 405100) et décompose inconditionnellement le montant avec VAT_VALUE = 20D (amountWithoutTaxes + taxesAmount à 20%). Les opérations d'assurance sont hors du champ de la TVA française (elles supportent la taxe sur les conventions d'assurance à la place), et un remboursement d'indemnité est un pur flux de bilan ; enregistrer une ventilation fictive de TVA déductible à 20% sur ces factures fausse toute déclaration de TVA dérivée des lignes de facture, même si l'écriture de grand livre elle-même porte le montant brut sur 405100.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsurancePaybackInvoiceCreationStrategy.java:57adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsurancePaybackInvoiceCreationStrategy.java:118
Contre-vérification — InsurancePaybackInvoiceCreationStrategy.java:57,119-121 code en dur, de façon vérifiable, une décomposition de TVA à 20% (amountWithoutTaxes, vatRate, taxesAmount) sur chaque ligne de facture de remboursement GLI contre le compte 405100. La matrice de la doc (adb-doc/docs/concepts/accounting/events/gli-payment-payback.md) spécifie une écriture brute Débit 405100 / Crédit 512101 sans TVA, et l'art. 261 C 2° du CGI exonère effectivement les opérations d'assurance. L'impact est borné : aucune règle de comptabilisation ni le FEC ne consomment les champs TVA des lignes (aucun compte 445xxx dans adb-accounting), donc seuls les métadonnées de facture / l'UI / les vues (adb-views InvoiceItemEntity) affichent la ventilation fictive. Sévérité basse correcte.
Je ne peux pas confirmer le traitement fiscal de ce remboursement au regard des règles françaises. Ce comportement a toutefois été discuté avec Pierre et revu par lui avant la mise en production.
Lorsque l'assureur GLI verse une indemnité pour loyers impayés, le circuit qui doit reverser cet argent au propriétaire ne peut jamais se déclencher : le cas est bloqué par un filtre qui exclut justement les encaissements d'indemnités GLI. En pratique, le propriétaire ne reçoit pas l'indemnité via le traitement normal, et si le gestionnaire tente un contournement par écriture manuelle, le reversement est comptabilisé comme un simple loyer locataire, dans le mauvais journal. Résultat : de l'argent encaissé qui reste bloqué chez l'agence ou des CRG faux, avec un traitement manuel obligatoire pour chaque sinistre.
Le transfert GLI_TO_OWNER est inatteignable : le propriétaire ne reçoit jamais l'indemnité GLI
Les seuls producteurs de GliToOwner (CallForRent/CallForRentalCharges/CFR-waste/ManualEntryEvent postSettlement) exigent le contexte CREDIT_EVENT_TYPE == GLI_INDEMNITIES_RECEIVED, qui provient de settlement.settlingAccountingEventType = l'eventType de l'écriture de lettrage. Or PostSettlementsProcessorImpl.isEligibleEntry n'accepte que les écritures de lettrage de type SUPPLIER_PAYMENT_ISSUED, TENANT_PAYMENT_RECEIVED ou MANUAL_ENTRY(+natures autorisées). Une véritable écriture GLI_INDEMNITIES_RECEIVED est filtrée, donc aucun post-lettrage ne s'exécute ; une MANUAL_ENTRY de nature GLI passe l'éligibilité mais produit CREDIT_EVENT_TYPE=MANUAL_ENTRY, si bien que CallForRent tombe dans la branche else et comptabilise TENANT_TO_OWNER (mauvais événement/journal). Scénario : indemnité comptabilisée (512101 D/405100 C), l'utilisateur lettre l'appel de loyer CALL_FOR_RENT ouvert du locataire contre le crédit 405100 via la modale d'écritures GLI de l'UI → SQS processSettlementMessage abandonne le lettrage → le 466100 du propriétaire n'est jamais crédité, le lettrage reste transferred=false indéfiniment. Le test unitaire CallForRentTest.postSettlement_forGli encode une intention que le pipeline ne peut jamais déclencher.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:49adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:201adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForRent.java:103adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEvent.java:165
Contre-vérification — Re-dérivé : GliToOwner exige CREDIT_EVENT_TYPE==GLI_INDEMNITIES_RECEIVED (CallForRent.java:103), défini uniquement par buildContext (PostSettlementsProcessorImpl.java:213) à partir de settlingAccountingEventType, que chaque writer dérive de l'eventType de l'écriture de lettrage (Settlement.java:122, SettlementServiceImpl.java:358). isEligibleEntry (PostSettlementsProcessorImpl.java:201-206) n'accepte que SUPPLIER_PAYMENT_ISSUED/TENANT_PAYMENT_RECEIVED/MANUAL_ENTRY, donc les véritables écritures de lettrage GLI_INDEMNITIES_RECEIVED (405100) issues de la modale GLI de l'UI (payment.service.ts:461) sont silencieusement abandonnées dans processSettlementMessage (AccountingFunctions.java:202) ; MANUAL_ENTRY de nature GLI produit CREDIT_EVENT_TYPE=MANUAL_ENTRY→TenantToOwner. Aucun autre producteur de GLI_TO_OWNER n'existe ; la branche était déjà morte avant le monorepo. Le 466100 du propriétaire n'est jamais crédité ; seuls les tests passent le contexte directement.
Le correctif requiert deux changements. (1) PostSettlementsProcessorImpl : ajouter GLI_INDEMNITIES_RECEIVED à ALLOWED_CREDIT_EVENT_TYPE, retirer MANUAL_ENTRY de cette liste, conserver le contrôle par nature isAllowedManualEntry, et retirer la nature GLI inutilisée d'ALLOWED_POST_SETTLEMENT_NATURES. (2) Synchroniser le seed de journaux du dépôt avec la collection de production maintenue à la main : une liste valide contient 13 paires code/description uniques (n°13 = GLI_TO_OWNER). [Note audit : le point (1) recoupe le correctif de C6 — retirer MANUAL_ENTRY réactive le garde-fou par nature.]
L'écran permet d'enregistrer qu'un locataire rembourse la GLI à l'assureur ou au propriétaire, mais dans ces deux cas le paiement saisi ne génère aucune écriture comptable : l'argent est enregistré côté trésorerie sans contrepartie dans les comptes. La créance douteuse constatée lors de l'indemnisation n'est donc jamais soldée, le suivi de la subrogation reste gonflé en permanence, et le CRG du propriétaire n'apprend jamais que le locataire a remboursé. L'écriture prévue par la documentation pour ce cas n'a tout simplement jamais été implémentée.
Les natures de remboursement TENANT_PAYBACK_GLI_INSURANCE et _OWNER ne génèrent aucune écriture au grand livre ; l'écriture GLI_PAYMENT_PAYBACK documentée (405100→512101) n'est implémentée nulle part
InsurancePaybackPaymentEvent est un porteur (postingRuleGenerator=FALSE) et friends() ne produit d'événements que pour AGENCY_COMPENSATION ; pour les deux autres natures proposées par l'UI, il renvoie une liste vide. Un utilisateur enregistrant « Locataire rembourse GLI à l'assureur » ou « au propriétaire » comptabilise un paiement qui ne laisse aucune trace au grand livre : le débit d'irrécouvrable 416100 issu de l'indemnité n'est jamais soldé, le suivi de la subrogation reste indûment gonflé, et le CRG/grand livre propriétaire n'a jamais connaissance du recouvrement. L'écriture spécifiée GLI_PAYMENT_PAYBACK (débit 405100 / crédit 512101, adb-doc gli-payment-payback.md) n'existe dans aucune règle de comptabilisation — Payment.createPostingRule n'a pas de cas GLI_PAYMENT_PAYBACK et lèverait « Unsupported event type » si jamais atteint.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InsurancePaybackPaymentEvent.java:64adb-accounting/src/main/resources/CATALOG_fr.properties:44adb-ui/src/app/content/pages/accounting/gli-tenant-payback/gli-tenant-payback.component.ts:115adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Payment.java:239
Contre-vérification — Re-dérivé : l'UI propose les 3 natures TENANT_PAYBACK_GLI_* (gli-tenant-payback.component.ts:115, CATALOG_fr.properties:44-46) et les envoie en compensationType (accounting.mapper.ts:242). AbstractPaymentStrategy.java:58-62 les enveloppe dans InsurancePaybackPaymentEvent (postingRuleGenerator=FALSE, ligne 56) ; friends() (lignes 64-98) n'émet des événements que pour AGENCY. Aucun validateur ne restreint la nature (PreValidationHandler.java:37-46 vérifie seulement l'existence des contrats d'assurance). DefaultHandlerChain.java:58 marque le porteur sans écriture comme PROCESSED, donc le paiement passe BOOKED sans aucune écriture ; le 416100 reste non soldé. Une sous-affirmation est exagérée : l'écriture documentée 405100→512101 EST bien réalisée pour la branche AGENCY via InsurancePaybackInvoiceCreationStrategy (clés de fabrique TenantToBadDebtClearEvent, InvoiceCreationStrategyFactoryImpl:43 ; AccountingFunctions.java:44-49 la déclenche). La gravité haute est maintenue : omission silencieuse au grand livre sur 2 des 3 options proposées.
Doublon du point déjà signalé côté relances. À ce jour seul AGENCY_COMPENSATION est implémenté. Les deux autres options ont été exposées dans l'UI par erreur, alors qu'aucune spécification correspondante n'existe encore dans l'Event Matrix.
Le système ajoute systématiquement 20 % de TVA à la prime d'assurance GLI et comptabilise le montant TTC ainsi gonflé : l'agence enregistre devoir à l'assureur, et refacture au propriétaire, 120 % de la prime réelle. Or les primes d'assurance sont exonérées de TVA en France (elles relèvent d'une taxe spécifique) : le taux est faux dans son principe comme dans son montant. Le même défaut appose une ventilation TVA fictive sur les remboursements d'indemnités, qui ne portent aucune taxe. Conséquences : propriétaires surfacturés, dettes fournisseur surévaluées et chiffres de TVA erronés sur les factures.
La facture de prime GLI code en dur une TVA à 20 % et comptabilise le montant TTC gonflé, alors que les primes d'assurance françaises sont exonérées de TVA
InsuranceContractInvoiceCreationStrategy calcule item.amount = amountWithVat(rate.calculateAmount(basis), 20.0) et InvoicePostingRuleFactory comptabilise item.getAmount() (le montant TTC) au compte de charge 616500 et au fournisseur 401100. L'agence enregistre donc une dette envers l'assureur, et refacture au propriétaire, à hauteur de 120 % de la prime GLI réelle. En France, les primes d'assurance sont exonérées de TVA (CGI art. 261 C 2°) et supportent à la place la taxe sur les conventions d'assurance — le taux de 20 % est erroné en nature comme en montant. InsurancePaybackInvoiceCreationStrategy appose de la même façon une ventilation de TVA fictive à 20 % (montant/1,2 + TVA) sur un remboursement d'indemnité qui ne supporte aucune taxe, produisant des montants de TVA faux sur les factures et dans toute déclaration de TVA.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:72adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:202adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/InvoicePostingRuleFactory.java:91adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsurancePaybackInvoiceCreationStrategy.java:119
Contre-vérification — Mécanique re-dérivée : InsuranceContractInvoiceCreationStrategy.java:72,202-205 majore la prime au taux d'achat d'un 20 % codé en dur (TaxCalculator.amountWithVat) ; Invoice.java:232 → InvoicePostingRuleFactory.java:91 comptabilise le TTC au 616500 et crédite l'auxiliaire assureur 401100 (LedgerAccountShort.java:203). Aucune écriture 44566x/44571, aucune TCA nulle part dans le monorepo ; la stratégie de remboursement (:57,117-121) fabrique une ventilation TVA à 20 % sur un remboursement d'indemnité hors champ. Gravité abaissée : InsuranceOption.buyRateWithoutVAT/sellRateWithoutVAT montre une conception délibérée en taux HT, et sous la doctrine du contrat-groupe une refacturation aux propriétaires avec TVA à 20 % est standard ; les factures de prime sont PARKED pour validation humaine ; la TVA du remboursement n'est que métadonnée (le grand livre comptabilise le montant total).
Convenu avec Pierre. Pour l'implémentation actuelle, la TVA est codée en dur à 20 % pour tous les paiements. Une configuration de taxe plus souple était prévue pour une étape ultérieure.
Le contrat d'assurance prévoit deux tarifs : le tarif d'achat (agence vers assureur) et le tarif de revente (agence vers propriétaire), et les deux sont bien saisis et conservés. Mais seul le tarif d'achat est réellement utilisé pour la facturation : le tarif de revente n'est exploité nulle part. Le propriétaire est donc facturé au prix d'achat de l'agence, et toute la marge négociée avec l'assureur est abandonnée sans que personne ne le voie — la donnée saisie à la signature n'a aucun effet. C'est un manque à gagner récurrent sur chaque prime GLI où un écart de taux existe.
sellRateWithoutVAT est stocké mais jamais utilisé : le propriétaire est facturé au taux d'achat de l'agence, la marge GLI n'est jamais facturée
InsuranceOption porte à la fois buyRateWithoutVAT (agence→assureur) et sellRateWithoutVAT (agence→propriétaire), et InsuranceSignedUpdateAction maintient les deux. Pourtant le seul taux jamais consommé est getLatestBuyRate dans la stratégie de facture de prime ; getLatestSellRate n'a aucun site d'appel dans le monorepo (grep sur getLatestSellRate/sellRate hors entité/modèle). La prime refacturée au propriétaire (via la facture entrante dont la ligne est affectée au contrat de location et lettrée sur le propriétaire) est donc la prime d'achat, et non le taux de vente contractuel — tout écart négocié par l'agence avec l'assureur est silencieusement offert, et les données de taux de vente saisies à la signature du contrat n'ont aucun effet.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/InsuranceOption.java:61adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:187adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/action/insurance/InsuranceSignedUpdateAction.java:44
Contre-vérification — getLatestSellRate (InsuranceOption.java:61) n'a aucun site d'appel dans tout le monorepo ; sellRateWithoutVAT n'est qu'écrit (InsuranceSignedUpdateAction.java:44, formulaire UI create-gli où il est requis). Le seul taux jamais consommé est getLatestBuyRate à InsuranceContractInvoiceCreationStrategy.java:187, et cette facture est adressée au propriétaire : receiver = insuranceContract.getToReceiver() (ligne 148), qui pour la GLI est le propriétaire (adb-ui contract.mapper.ts:1211 mappe gliModel.owner vers toReceiver ; la fixture insurance_contract.json le confirme ; findExistsInvoice lignes 216-222 apparie le receiver par ownerId). Aucun flux alternatif de taux de vente n'existe. Gravité moyenne appropriée : l'écart n'est perdu que lorsque buy != sell.
Également convenu avec Pierre. Facturer l'écart entre le taux d'achat et le taux de vente, marge GLI incluse, a été volontairement reporté à une implémentation ultérieure.
Quand le tarif de la prime GLI prend effet exactement le jour de la facturation — cas fréquent, un tarif effectif au premier ou dernier jour de la période couverte —, le système considère qu'aucun tarif n'est applicable. Au lieu de signaler le problème, il abandonne discrètement la ligne de facture : la prime n'est pas appelée pour cette période et personne n'est prévenu, seule une trace technique est laissée dans les journaux internes. Des primes d'assurance peuvent donc passer à la trappe sans que le gestionnaire s'en aperçoive.
Le taux de prime en vigueur À la date de comptabilisation est ignoré et l'erreur résultante est silencieusement absorbée — la prime GLI n'est jamais facturée
InsuranceOption.getLatestRate ne conserve que les taux tels que `ChronoUnit.DAYS.between(whenEffective, postingDate) > 0`, si bien qu'un taux dont whenEffective est égal à la date de comptabilisation (cas courant : taux en vigueur le premier/dernier jour de la première période couverte) est exclu. Si c'est le seul taux applicable, getLatestBuyRate renvoie null et buildInvoiceItem émet PREMIUM_RATE_NOT_FOUND — que buildInvoiceItems convertit en Mono.empty() après une ligne de log. La ligne de facture est silencieusement abandonnée : aucune prime n'est appelée pour cette option/période et personne n'est alerté. Entrées : contrat d'assurance avec taux d'achat en vigueur le 2026-07-31, facture de prime avec postingDate 2026-07-31 → aucune facturation, seul un avertissement de log.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/InsuranceOption.java:78adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:161
Contre-vérification — Re-dérivé : InsuranceOption.java:78 utilise `difference > 0`, donc un taux dont whenEffective est égal à postingDate est ignoré ; aucune validation adb-contracts ne l'empêche, et InsuranceContractInvoiceCreationStrategy.java:161-163 absorbe le PREMIUM_RATE_NOT_FOUND résultant (:210) via onErrorResume(Mono.empty()), abandonnant silencieusement la ligne de prime. Le déclencheur est toutefois plus étroit qu'annoncé : postingDate est la FIN de période (periodEndDate, :376-386), donc les taux du premier jour de période fonctionnent ; seul whenEffective == date de fin de période échoue. De plus, la facture est PARKED (isAutomaticBooking=false), donc la ligne manquante est révisable par un humain. Décalage d'un jour + absorption silencieuse confirmés ; abaissé de moyenne→basse.
Problème valide. Quand whenEffective est égal à postingDate, l'écart calculé vaut 0, mais la condition actuelle n'accepte que les valeurs strictement supérieures à 0. Le correctif consiste simplement à passer de difference > 0 à difference >= 0 dans fr/lifeconnect/adb/model/contract/InsuranceOption.java:78.
Pour déterminer les bornes de la période de prime, le système compare uniquement le mois, en ignorant l'année. Si l'on facture ou refacture une période d'une année passée (ou future) dont le mois coïncide avec le mois calendaire en cours, une période trimestrielle est ramenée à un seul mois : la facture assureur porte alors de mauvaises dates et un mauvais libellé de période. Exemple : le 2 juillet 2026, l'appel de prime de juillet 2025 en paiement trimestriel couvre le seul mois de juillet 2025 au lieu du trimestre. Impact limité aux régularisations à cheval sur les années, mais factures fausses dans ce cas.
Les bornes de période de prime comparent le mois en ignorant l'année
periodBeginDate/periodEndDate arbitrent entre les bornes du « mois courant » et celles de premiumPaymentPeriod (par ex. trimestrielle) via `YearMonth.now(timeZone).getMonth().equals(period.getMonth())` — l'année est abandonnée. Facturer ou refacturer une période d'une année antérieure (ou postérieure) dont le mois coïncide avec le mois calendaire courant emprunte la mauvaise branche : une période de prime trimestrielle est réduite à un seul mois, donnant un postingDate/documentDate de facture erroné et un libellé de période erroné sur la facture de l'assureur. Exemple : le 2026-07-02, générer l'appel de prime pour la période 2025-07 (échéance de paiement trimestrielle) utilise 2025-07-01→2025-07-31 au lieu des bornes du T3 2025.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:364adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/insurance/InsuranceContractInvoiceCreationStrategy.java:376
Contre-vérification — InsuranceContractInvoiceCreationStrategy.java:366/378 ne comparent que le mois, en abandonnant l'année ; InvoiceServiceImpl.java:258/289-294 laisse les utilisateurs facturer n'importe quel callForRentPeriod passé, donc facturer 2025-07 (trimestriel) en juillet 2026 emprunte à tort la branche du mois courant : periodEndDate=2025-07-31 au lieu de la fin de trimestre 2025-09-30, corrompant postingDate/documentDate (lignes 125,149-150,177), le libellé de période et la recherche de taux getLatestBuyRate(postingDate) (ligne 187). Correction : periodBeginDate n'est pas affecté — le getPeriodStartDate() de la branche else renvoie le même period.atDay(1). Le déclenchement nécessite un décalage exact de N*12 mois, une fréquence non mensuelle, aucune facture existante ; la facture est PARKED pour comptabilisation manuelle → basse.
Problème mineur mais valide. La comparaison devrait inclure l'année, ou comparer directement la valeur YearMonth complète au lieu du seul mois.
Tout paiement enregistré avec le mode de règlement GLI est traité comme une indemnité reçue de l'assureur, sans vérifier si l'argent entre ou sort. Si l'agence rembourse un trop-perçu d'indemnité en utilisant ce mode, la comptabilité augmente le solde de banque au lieu de le diminuer et gonfle à tort le compte de créances douteuses. Par ailleurs, il suffit qu'un paiement porte un type de compensation quelconque pour être requalifié en remboursement GLI, quel que soit son mode de règlement réel. Ces cas restent marginaux, mais quand ils surviennent la banque et le suivi des créances sont faux.
Le sens du paiement est ignoré lors du classement des paiements GLI ; tout compensationType détourne un paiement vers un remboursement GLI
AbstractPaymentStrategy.paymentEvent mappe tout paiement de paymentMethod PAYMENT_METHOD.GLI vers InsurancePaymentReceived (→ GLI_INDEMNITIES_RECEIVED, qui DÉBITE toujours la banque et ajoute un débit d'irrécouvrable 416100) sans vérifier le sens du flux. Un remboursement sortant d'une indemnité trop perçue, enregistré avec la méthode GLI, augmenterait le 512101 au lieu de le diminuer et gonflerait à tort le compte de créances douteuses. Symétriquement, tout paiement portant un compensationType non nul — champ qui est une valeur générique du catalogue ACCOUNTING_EVENT_NATURE positionnable via l'API de paiement — est routé vers InsurancePaybackPaymentEvent quelle que soit la méthode de paiement, et si la nature n'est pas TENANT_PAYBACK_GLI_AGENCY il ne comptabilise rien (voir constat 3).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/processing/impl/AbstractPaymentStrategy.java:52
Contre-vérification — Re-dérivé du code : AbstractPaymentStrategy.java:54-66 ne consulte le sens que dans la branche else ; la méthode GLI produit toujours InsurancePaymentReceived dont les écritures secondaires (PostingRuleFactory.java:84-148) débitent inconditionnellement la banque et l'irrécouvrable 416, donc un paiement SORTANT de méthode GLI est comptabilisé comme un encaissement. Aucun garde-fou n'existe : Payment.java:44-78 n'a aucune validation inter-champs, PaymentController.java:38 lie directement l'objet de domaine, et le formulaire d'envoi de paiement en prod propose le catalogue PAYMENT_METHOD non filtré (accounting-payment-form.component.ts:144, payment.service.ts:109-142), rendant le scénario atteignable. compensationType détourne de même le routage indépendamment de la méthode (ligne 58). Nécessite un mauvais usage opérateur/API, donc gravité basse maintenue.
Non confirmé au vu de l'implémentation actuelle. InsurancePaymentReceived est un événement enveloppe abstrait qui produit InsuranceIndemnitiesReceived et TenantToBadDebtEvent conformément à la spécification.
Toutes les créances douteuses liées aux sinistres GLI sont regroupées sur un seul compte pour l'ensemble de l'agence, sans détail par locataire : impossible de suivre en comptabilité ce que chaque locataire subrogé a remboursé. De plus, un remboursement est accepté sans vérifier qu'une indemnité a réellement été versée pour ce locataire ni que le montant ne dépasse pas la créance : un remboursement trop élevé, ou saisi sur un bail qui n'a jamais eu de sinistre, passe sans alerte et fausse le compte. Le rapprochement des recouvrements devient un travail manuel, avec un risque d'erreurs non détectées.
Le compte de locataires douteux 416100 est au niveau organisation, sans auxiliaire ni validation du montant remboursé
tenantBadDebts(organizationId) construit un unique compte général 416100 pour toute l'agence (pas d'auxiliaire par locataire comme 411100/RC), si bien que les créances subrogées de tous les locataires sous GLI sont mutualisées et que le recouvrement par locataire ne peut être rapproché depuis le grand livre. TenantToBadDebtClear crédite le 416100 du montant total du remboursement sans vérifier qu'un débit d'irrécouvrable d'un montant égal ou supérieur existe pour ce locataire — PreValidationHandler.validateGliPayback vérifie seulement que le contrat de location a UN contrat lié (pas qu'il s'agit d'une assurance GLI, active, ni qu'une indemnité a été reçue). Un remboursement supérieur à l'indemnité (ou un remboursement sur un contrat n'ayant jamais eu de sinistre) rend silencieusement le 416100 négatif.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/LedgerAccountShort.java:249adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:152adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/PreValidationHandler.java:66
Contre-vérification — LedgerAccountShort.java:249 construit le 416100 en gl() au niveau organisation (tenant() à :150 utilise un auxiliaire par contrat). PostingRuleFactory.java:157-176 crédite le 416100 de payment.getAmount() sans condition ; le grep confirme l'absence de tout contrôle de solde. PreValidationHandler.java:66-70 n'exige qu'un relatedContracts non vide (List<TargetObject> générique, AbstractContract.java:87) — aucun contrôle de type GLI/actif/indemnité existante. L'UI (gli-tenant-payback.component.html:167) n'impose qu'un min=0.01. Défaillance re-dérivée : un remboursement manuel > indemnité rend le 416100 mutualisé négatif, masqué par les autres locataires. Gravité basse justifiée : flux manuel, conforme à la matrice d'événements documentée (tenant-to-bad-debt-clear.md D800000/C416100), et le recouvrement par locataire reste traçable via les cibles d'événements/factures.
L'écriture elle-même suit la spécification de l'événement TENANT_TO_BAD_DEBT : DÉBIT 416100 → CRÉDIT 800000.
Lorsqu'un bail arrive au terme de sa durée initiale (par exemple 3 ans) et se reconduit tacitement, le système enregistre bien la reconduction mais ne repousse jamais la date de fin du contrat. Résultat : dès le mois suivant la fin initiale, plus aucun appel de loyer n'est émis pour ce bail, alors que le locataire est toujours en place. Aucune alerte n'est produite : les loyers ne sont ni appelés ni quittancés, ce qui signifie des encaissements manqués, des CRG faux pour les propriétaires et une régularisation manuelle lourde une fois le problème découvert.
La reconduction tacite n'étend jamais la date de fin persistée du contrat : les appels de loyer s'arrêtent silencieusement au terme initial
Chaque contrat de location voit end = begin + duration - 1 persisté lors du patch de l'écran des conditions (workflow de brouillon obligatoire) : setCalculatedFields le calcule lorsqu'il est null et l'enrichissement MapStruct le recopie vers l'entité. Le job de reconduction (renewContracts -> RentalContract.renewContract -> updateContractRenewedDates) ne met à jour QUE nextRentRenewalDate et lastContractRenewalDates, jamais end. Le filtrage des contrats candidats à une période de loyer exclut ceux dont end précède le début de la période (isPeriodValidForContract), et buildNextCallForRentPeriod marque la période contenant end comme ENDED/FINAL. Scénario : bail vide de 3 ans signé le 2023-09-01, tacitRenewal=true, le locataire reste ; après le 2026-08-31 le contrat est reconduit par le job (nextRentRenewalDate -> 2029) mais disparaît des candidats à l'appel de loyer et sa chaîne d'appels est ENDED — plus aucun loyer n'est jamais quittancé pour un bail pourtant légalement reconduit (art. 10 loi 89-462).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:714adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/ContractUpdateRepositoryImpl.java:68adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:171adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1420
Contre-vérification — Redérivé indépendamment. updateContractRenewedDates n'écrit que nextRentRenewalDate/lastContractRenewalDates (ContractUpdateRepositoryImpl.java:68-77) ; renewContract (RentalContract.java:1084-1103) ne touche jamais à end ; tous les autres appels à setEnd relèvent des flux de congé/vente/annulation. end=begin+duration-1 est toujours persisté (ContractServiceImpl.java:991-995) et le champ UI est en lecture seule (rc-conditions.component.html:306), donc aucune extension manuelle n'existe. Le filtrage des appels de loyer exclut les périodes postérieures à end (ContractHelper.java:176-184 via ContractAccountingServiceImpl.java:513) et la dernière période est marquée ENDED pour les contrats SIGNED (RentalContract.java:1420-1430) sans chemin de réinitialisation (ContractStateServiceImpl.java:219). tacitRenewal n'est jamais consulté dans la logique d'appel de loyer. Les appels de loyer s'arrêtent silencieusement au terme initial malgré la reconduction tacite.
Valide. renewContract() n'affecte pas la génération des CFR — implémentation ancienne, plus intégrée au flux actuel de reconduction/facturation. Pour la tacite reconduction, la date de fin devrait aussi être prolongée : endDate → nextRentRenewalDate, puis nextRentRenewalDate avancée à l'échéance suivante, lastContractRenewalDates conservant l'historique. Sans mise à jour d'endDate, la génération des CFR reste bornée par le terme d'origine et la facturation peut s'arrêter alors que le bail est tacitement reconduit. Pas certain à 100 % que ce fût l'intention exacte de Pierre/Marwan, mais je ne vois pas où synchroniser endDate casserait la logique existante — c'est le comportement le plus cohérent.
Le calcul de reconduction ne connaît que deux cas de propriétaire : personne physique (bail prolongé de 3 ans) et société (6 ans). Tous les autres cas — indivision, groupement, communauté, copropriété, colocation de propriétaires, propriétaire de type inconnu — sont ignorés sans aucun message d'erreur. Or l'indivision est extrêmement fréquente en gestion locative française. Pour ces baux, la reconduction ne se fait jamais et le traitement automatique retente indéfiniment à chaque passage, en silence. Les mêmes conséquences que l'arrêt du quittancement peuvent en découler, sans que personne ne soit alerté.
La chaîne de règles de reconduction ignore silencieusement les propriétaires qui ne sont pas PersonNatural/PersonCompany (indivision, groupement, union, inconnu) — les contrats ne se reconduisent jamais et sont resélectionnés à chaque exécution
RenewalEmptyCreatedContractAction / RenewalEmptyTakeOverContractAction ne calculent le nouveau terme que pour `owner instanceof PersonCompany` (+6 ans) ou `PersonNatural` (+3 ans). PersonType comporte 8 types concrets (PersonIndivision, PersonGroup, PersonUnion, PersonCondo, PersonFlatShare, PersonUnknown) et le mapper conserve le type concret (PersonMapperFactory). Pour un bail vide détenu en indivision — extrêmement fréquent dans les portefeuilles français, légalement 3 ans (personnes physiques) — nextRenewalDate reste null, renewContract() ne fait rien, et le job resélectionne le contrat à chaque exécution indéfiniment (requête = nextRentRenewalDate antérieur à maintenant) sans erreur. Même contournement silencieux pour les catégories CIVIL/COMMERCIAL/TEMPORARY/DEROGATION, qu'aucune règle ne couvre. De plus une SCI familiale (PersonCompany) reçoit +6 ans alors que l'art. 13 loi 89-462 l'assimile à une personne physique (3 ans).
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/renewed/empty/creation/RenewalEmptyCreatedContractAction.java:24adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/renewed/empty/takeover/RenewalEmptyTakeOverContractAction.java:23adb/adb-model/src/main/java/fr/lifeconnect/adb/model/person/PersonType.java:5adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:714
Contre-vérification — Redérivé : RenewalEmptyCreatedContractAction.java:24-28 et RenewalEmptyTakeOverContractAction.java:23-27 ne fixent nextRenewalDate que pour PersonCompany/PersonNatural ; les 8 types de personne étendent AbstractPerson directement (PersonIndivision.java:12), donc instanceof échoue pour indivision/groupement/union/inconnu. PersonMapperFactory.java:12 conserve le type concret depuis ShortPersonEntity. RentalContract.java:1095 rend renewContract() inopérant lorsque null ; ContractServiceImpl.java:716 resélectionne (nextRentRenewalDate antérieur à maintenant) à chaque exécution du cron (ContractRenewalJobExecutor.java:32) sans erreur — onErrorContinue ne se déclenche que sur exception. Aucun garde-fou en amont : la recherche de propriétaire de rc-step1 (owners.search.service.ts:52) n'impose aucun filtre de type de personne, et RentalContractCategory inclut les CIVIL/COMMERCIAL non gérés. L'art. 13 loi 89-462 assimile effectivement indivision/SCI familiale à une personne physique (3 ans). La sévérité haute est justifiée.
Problème valide. L'implémentation s'appuie sur RC-automatic-renewal.md et a probablement été limitée volontairement, pour la première mise en production, aux types de propriétaires utilisés par les clients de l'entreprise à l'époque. Or le modèle de domaine supporte désormais d'autres types de propriétaires, silencieusement ignorés.
Un contrôle existe pour vérifier qu'un congé locataire avec préavis réduit se termine bien dans une fenêtre raisonnable (entre hier et trois mois), mais il n'est jamais déclenché : tous les points d'entrée le désactivent. Un gestionnaire peut donc enregistrer un préavis réduit avec une date de départ déjà passée ou située dans plusieurs années. Le système en déduit alors une durée de préavis aberrante — supérieure au maximum légal de 3 mois, voire négative — et calcule une date de fin de bail fausse, avec des conséquences en chaîne sur les documents et le suivi du départ.
getValidatedNoticePeriodInMonths ne valide jamais : le contrôle de la fenêtre de préavis réduit est du code mort
noticePeriodInMonths prend un indicateur `validation` qui conditionne checkShortenedNoticePeriod (end doit se situer entre hier et maintenant+3 mois). Les deux points d'entrée publics — getNoticePeriodInMonths ET getValidatedNoticePeriodInMonths — passent `false`, donc le contrôle ne peut jamais s'exécuter. La transition NOTIFY accepte donc un préavis SHORTENED dont la date de fin est dans le passé ou des années dans le futur ; MONTHS.between(now, endDate) peut même renvoyer une valeur supérieure au maximum légal de 3 mois ou un nombre négatif, qui alimente ensuite noticeDate.plusMonths(months) et corrompt contract.end et theoreticalEndDate.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:109adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:362
Contre-vérification — ContractHelper.java:106 et :110 passent tous deux validation=false, donc checkShortenedNoticePeriod (:151) est inatteignable — la variante « Validated » ne valide rien. Aucune autre couche ne compense : ContractStateServiceImpl.java:358 ne rejette que endDate<noticeDate ; ContractNotifyEventInfo.endDate n'a aucune contrainte bean ; les validateurs de contrat (TenantFreeNoticePeriodRule, etc.) ne couvrent que les conditions de reprise de gestion ; la modale de congé d'adb-ui saisit librement la date de fin SHORTENED sans plafond de 3 mois. Redérivé : un préavis SHORTENED avec endDate=now+24m donne months=24 ; une endDate rétrodatée dans le passé donne un nombre de mois négatif, fixant theoreticalEndDate/contract.end avant la date du préavis (ContractStateServiceImpl.java:362-372). La sévérité moyenne se maintient.
Valide. Les deux méthodes publiques désactivent checkShortenedNoticePeriod, si bien que la méthode censée « valider » se comporte exactement comme la version sans validation. La validation a vraisemblablement été désactivée à cause de bugs antérieurs.
À la création d'un bail, le système vérifie seulement qu'une durée est renseignée, jamais qu'elle respecte la loi : 3 ans (bailleur personne physique) ou 6 ans (personne morale) pour un logement vide, 1 an en meublé, 9 mois sans reconduction pour un bail étudiant, 9 ans en commercial — et le bail mobilité n'existe même pas comme catégorie. Un gestionnaire peut donc signer un bail vide d'un an avec reconduction tacite pour un particulier : la validation passe, puis le moteur de reconduction le prolonge de 3 ans. Le cabinet s'expose à des baux juridiquement non conformes et à des litiges avec locataires ou propriétaires.
Aucune validation de la durée légale du bail pour aucun type de contrat
Les seuls contrôles de durée dans toute la chaîne de validation portent sur la présence et l'exclusivité mutuelle de rentalDurationNormal et rentalDurationShortened (validateRentalContractDates, utilisé par les validateurs de brouillon et de prêt-à-signer). Rien n'impose les planchers/formats légaux : 3 ans (personne physique) / 6 ans (personne morale) pour un logement vide, 1 an meublé, bail étudiant de 9 mois (qui doit aussi avoir tacitRenewal=false), bail mobilité de 1 à 10 mois (aucune catégorie n'existe pour lui), 9 ans commercial. Scénario : un opérateur crée un bail EMPTY_RESIDENTIAL de 1 an avec reconduction tacite pour un propriétaire personne physique — la validation passe, le contrat se signe, puis le moteur de reconduction l'étend de 3 ans à partir d'un terme légalement nul. Le contrôle de cohérence end/durée est lui aussi commenté (checkRentalContractEndDate).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/ContractValidatorUtils.java:101adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractDraftValidator.java:51adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.java:68
Contre-vérification — Vérifié : validateRentalContractDates (ContractValidatorUtils.java:101-119) ne contrôle que la nullité/exclusivité ; le contrôle de date de fin est commenté (87-93, 115-118) ; aucune règle de durée n'existe non plus dans les validateurs à base de règles. Aucune catégorie STUDENT/MOBILITY nulle part. Scénario redérivable : RenewalEmptyCreatedContractAction ajoute +3 ans (PersonNatural)/+6 ans (PersonCompany), prouvant que la règle légale est connue mais non appliquée à la création, tandis que nextRentRenewalDate = begin + durée saisie par l'opérateur (ContractServiceImpl:1088-1097). Atténuation : l'UI restreint les presets 1/2 ans au meublé (rc-conditions.component.html:70-101), mais le chemin de durée réduite autorise une saisie au niveau du mois sans motif/plancher/contrôle de reconduction tacite, et l'API contourne l'UI. La sévérité moyenne se maintient.
Certaines restrictions ont peut-être été implémentées côté frontend. Les bornes légales de durée côté backend ne faisaient pas partie des exigences convenues — à clarifier avec le métier/le juridique avant de qualifier de bug.
Le seul flux de congé disponible modélise uniquement le départ du locataire : motifs, préavis (1 mois, 3 mois ou réduit) et écran de saisie sont pensés pour le locataire. Un congé délivré par le propriétaire — préavis légal de 6 mois en logement vide pour vente, reprise ou motif légitime, 3 mois en meublé — ne peut tout simplement pas être enregistré, et aucun contrôle du motif ni de l'échéance du bail n'existe. Le gestionnaire qui traite un congé bailleur doit le suivre hors de l'outil, avec un risque d'oubli d'échéances légales et un angle mort complet sur ces dossiers dans le système.
Aucun flux de congé bailleur : NOTIFY ne modélise que le départ du locataire et ne peut représenter un préavis de 6 mois
L'unique transition NOTIFY porte des données propres au seul départ du locataire (catalogues TENANT_LEAVE_NON_COMPLIANCE_REASON, TENANT_LEAVE_SHORTEN_REASON ; la modale UI est 'rc-departure-notice'). Le switch de durée de préavis ne gère exactement que MONTH (1), QUARTER (3) et SHORTENED et lève une exception pour tout le reste, si bien qu'un congé bailleur légal de 6 mois (logement vide, pour vente/reprise/motif légitime) ou un congé bailleur meublé de 3 mois ne peut pas être enregistré du tout ; il n'y a aucune validation du motif du bailleur ni de la contrainte « congé uniquement à l'échéance ». Le congé bailleur n'existe que comme métadonnée contractuelle sur les contrats de reprise CIVIL (règles OwnerNoticeModality/OwnerFreeNoticePeriod), jamais comme événement de cycle de vie. Un gestionnaire délivrant un congé pour vente n'a aucun moyen conforme de résilier le bail dans le système, sinon de le déguiser en départ de locataire.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/event/ContractNotifyEventInfo.java:44adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:113adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/OwnerNoticeModalityRule.java:16
Contre-vérification — Redérivé indépendamment. ContractNotifyEventInfo.java:44-53 n'a que des catalogues TENANT_LEAVE_* ; ContractHelper.java:115-119 lève une exception pour tout noticePeriod autre que MONTH/QUARTER/SHORTENED et est invoqué à chaque NOTIFY (ContractStateServiceImpl.java:362), donc un congé bailleur de 6 mois est irreprésentable. La modale UI (rc-departure-notice-modal.component.html:3, data-cy="tenant-leave-modal") est réservée au locataire. Un grep sur tout le dépôt (congé/bailleur/reprise/owner/landlord notice) ne trouve le congé bailleur que comme métadonnée contractuelle statique (OwnerNoticeModalityRule, landlordNoticePeriod), jamais comme événement de cycle de vie ; TERMINATE ne porte qu'une simple date. Le template de bail RC.html:1237-1238 promet même le droit de congé du bailleur. La sévérité moyenne se maintient.
Pas encore implémenté. Le flux NOTIFY actuel ne couvre que le départ du locataire et ne gère ni le congé à l'initiative du bailleur ni le préavis correspondant.
Pour un congé avec préavis réduit, la date de fin théorique du bail n'est pas figée à l'enregistrement : elle est recalculée à chaque consultation en fonction de la date du jour. Elle recule donc au fil des semaines et peut finir par afficher une date antérieure au congé lui-même. Exemple : un congé posé le 10 janvier avec départ le 20 février affiche une fin théorique au 10 février en janvier, puis au 10 décembre de l'année précédente si on consulte en avril. Toute personne (gestionnaire, documents, écrans de conformité) voit une date mouvante et à terme absurde. L'impact reste un problème d'affichage et de fiabilité de l'information, pas de perte financière directe.
theoreticalEndDate pour les préavis SHORTENED est recalculée par rapport à l'horloge courante à chaque lecture de l'entité et dérive dans le temps
ContractAfterMapper.setRentalTheoreticalEndDate s'exécute à chaque mapping entité-vers-domaine et recalcule noticeDate.plusMonths(getNoticePeriodInMonths(...)). Pour SHORTENED, le nombre de mois est MONTHS.between(LocalDate.now(), contract.end), donc la date de fin théorique affichée/sérialisée change au fil du calendrier. Scénario : préavis enregistré le 2026-01-10 avec end au 2026-02-20 ; une lecture en janvier donne theoreticalEndDate 2026-02-10 ; une lecture en avril donne MONTHS.between = -1, donc theoreticalEndDate devient 2025-12-10 — antérieure au préavis lui-même. Tout consommateur en aval (UI, documents, affichage de conformité) voit une date mouvante et finalement absurde.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/ContractAfterMapper.java:337adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:124
Contre-vérification — Redérivé : chaque lecture entité→domaine exécute fill→setCalculatedFields→setRentalTheoreticalEndDate (ContractAfterMapper.java:186,270,337-343) ; pour SHORTENED elle appelle shortenedNoticePeriodInMonths avec validation=false, renvoyant MONTHS.between(LocalDate.now(), end) (ContractHelper.java:105-106,124-131), donc theoreticalEndDate dérive quotidiennement et devient négative une fois end dépassé (noticeDate−1 mois, avant le préavis). L'entité n'a pas de champ theoreticalEndDate, la base n'est donc pas touchée. Sévérité abaissée à basse : aucun consommateur n'existe — le grep ne trouve aucun appelant de getTheoreticalEndDate dans un service Java, aucun binding de template UI (seulement la déclaration de type à adb-ui/src/app/core/models/contract/rental-contract-model.ts:118), aucun usage dans views/reports. L'impact est un champ d'API dérivant et inutilisé.
Ne paraît pas être un bug. theoreticalEndDate est volontairement une valeur dynamique, calculée ; pour les préavis réduits, elle est censée être recalculée par rapport à la date du jour.
Sur certains contrats civils repris en gestion, le contrôle du préavis libre du locataire renvoie, en cas de problème (lot manquant ou invalide), un message d'erreur libellé comme portant sur le préavis du propriétaire. Le gestionnaire est donc orienté vers le mauvais champ et perd du temps à chercher le problème au mauvais endroit. C'est un défaut d'étiquetage du message d'erreur, sans impact financier, mais il génère de la confusion et du travail inutile.
TenantFreeNoticePeriodRule signale ses échecs sous le code d'erreur du bailleur (Owner)
Les deux branches d'échec de la validation unitaire du préavis libre du locataire lèvent ErrorCodes.OwnerFreeNoticePeriodError au lieu de TenantFreeNoticePeriodError (copier-coller depuis OwnerFreeNoticePeriodRule). Un contrat de reprise CIVIL avec une modalité de préavis locataire WHENEVER et une unité manquante/invalide fait remonter un code d'erreur 'Owner Free Notice Period' vers l'API/l'UI, orientant l'opérateur vers le mauvais champ ; seule la première branche (période null) utilise le bon code.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/TenantFreeNoticePeriodRule.java:38
Contre-vérification — TenantFreeNoticePeriodRule.java:38,43 lèvent toutes deux ErrorCodes.OwnerFreeNoticePeriodError avec le texte "Tenant Free Notice Period unit..." — un copier-coller depuis OwnerFreeNoticePeriodRule.java:38,42 ; seule la branche période-null (ligne 25) utilise TenantFreeNoticePeriodError. Les deux constantes d'enum existent distinctement dans ErrorCodes.java:10,12, aucun test ne fige le comportement, et adb-ui n'analyse pas ces codes, donc l'impact est un préfixe de code erroné dans la chaîne d'erreur de l'API alors que le texte du message nomme toujours le bon champ. La validation elle-même fonctionne ; la sévérité basse est exacte.
Bug de copier-coller valide. La validation locataire renvoie OwnerFreeNoticePeriodError dans ses deux branches d'échec ; elle devrait renvoyer TenantFreeNoticePeriodError.
Lors d'une reprise de gestion, on peut enregistrer un avoir locataire que l'ancien gestionnaire n'a pas remis à l'agence (par exemple 300 EUR). Le contrôle censé empêcher que cet avoir déclenche un reversement au propriétaire ne fonctionne pas : dès que l'avoir vient s'imputer sur le prochain appel de loyer, le système crédite le compte du propriétaire comme si l'agence avait réellement encaissé cet argent. L'agence reverse donc au propriétaire des fonds qu'elle n'a jamais reçus, à sa propre perte. Constat critique : chaque reprise avec un avoir non remis en espèces expose directement la trésorerie de l'agence.
Les crédits de reprise CREDITOR_NOT_CASH déclenchent un reversement au propriétaire : l'agence verse aux bailleurs des fonds qu'elle n'a jamais reçus
Le filtre d'éligibilité côté lettrage est cassé : MANUAL_ENTRY est listé sans condition dans ALLOWED_CREDIT_EVENT_TYPE, si bien que la restriction sur la nature dans isEligibleEntry (ALLOWED_POST_SETTLEMENT_NATURES : creditor CASH + les 3 natures debitor + GLI) est du code mort — n'importe quel crédit manuel est admis. Scénario : reprise de gestion avec un crédit locataire de 300 EUR NON remis par l'ancien gestionnaire → l'agence comptabilise ACCOUNTING_TAKEOVER_TENANT_BALANCE_CREDITOR_NOT_CASH (débit 467200 / crédit 411100/RC). Lorsque ce crédit lettre le débit du prochain CALL_FOR_RENT (sa finalité documentée : « sert au lettrage, aucune écriture propriétaire après lettrage »), CallForRent.postSettlement déclenche inconditionnellement TenantToOwner → débit 800000 / crédit 466100/RDC → le propriétaire est crédité de 300 EUR sans contrepartie en trésorerie, puis payé via OWNER_PAYMENT_ISSUED. Même fuite pour les remises de loyer ou les crédits de restitution de dépôt qui lettrent des appels de loyer. Aucun test ne couvre isEligibleEntry.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:49adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:201adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForRent.java:115adb-doc/docs/concepts/accounting/events/accounting-takeover-tenant-balance-creditor-not-cash.md:20
Contre-vérification — Redémontré de bout en bout. CREDITOR_NOT_CASH est comptabilisé en MANUAL_ENTRY avec une nature (RentPostingRuleFactory.java:358-372, ~795 ; EntryHandler.java:52-53). PostSettlementsProcessorImpl.java:49-53 liste MANUAL_ENTRY sans condition, rendant la liste blanche de natures en :201-206 du code mort (isAllowedManualEntry est un sous-ensemble strict de isAllowedEventType). Les écritures CALL_FOR_RENT lettrées passent preValidation (AccountingFunctions.java:51-62), et CallForRent.java:115-123 émet TenantToOwner inconditionnellement (CREDIT_EVENT_TYPE="MANUAL_ENTRY"≠GLI). La doc confirme l'intention : creditor-cash « déclenche une écriture propriétaire », creditor-not-cash « aucune écriture propriétaire » — la liste blanche reflète exactement cela. Aucune exclusion en amont du lettrage, aucun garde-fou sur TenantToOwner, aucun test ne couvre isEligibleEntry. Reversements propriétaire sans contrepartie dans un flux normal et documenté : critique.
Problème valide et critique. Correctif : retirer MANUAL_ENTRY de ALLOWED_CREDIT_EVENT_TYPE dans PostSettlementsProcessorImpl.java:49-53.
Le compte rendu de gestion envoyé aux propriétaires filtre les écritures selon une liste qui ne correspond pas à la façon dont les reprises de solde et les ajustements divers (réparations locatives, régularisations locataire, etc.) sont réellement enregistrés. Résultat : toutes ces lignes disparaissent purement et simplement du CRG. Le propriétaire reçoit un relevé dont le détail ne reflète pas la comptabilité réelle, ce qui fausse sa lecture et peut générer des réclamations et un risque de responsabilité vis-à-vis du mandant.
Le CRG (compte rendu de gestion) propriétaire supprime silencieusement toutes les lignes de solde de reprise (et les écritures manuelles diverses) : l'eventType est comparé à des valeurs d'énumération qui ne sont que des natures
Le filtrage du CRG (compte rendu de gestion) conserve les écritures dont l'eventType figure dans MISCELLANEOUS_EVENT_TYPES, qui liste ACCOUNTING_TAKEOVER_TENANT_BALANCE_DEBITOR / _CREDITOR_NOT_CASH / _CREDITOR_CASH, REPAIRS_DUE_BY_TENANT, TENANT_ADJUSTMENT, etc. Mais ces constantes n'existent jamais que comme NATURES d'événement : toute écriture de reprise/ajustement est persistée avec eventType=MANUAL_ENTRY (ManualEntryEvent le fixe en dur ; EntryHandler le recopie sur l'écriture ; la nature va dans le champ distinct eventNature). MANUAL_ENTRY n'est pas dans la liste, donc postProcessEntriesByRentalContract supprime toute ligne de reprise et d'ajustement divers, et amountByEventType renvoie null pour ces lignes. Scénario : l'agence reprend un locataire avec 500 EUR d'arriérés (DEBITOR) → le CRG du propriétaire n'affiche aucune ligne de reprise et un solde locataire faussé (seule la contrepartie de lettrage apparaît, faisant apparaître un locataire débiteur comme créditeur).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:166adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:264adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/impl/CRGPeriodRecapFactoryImpl.java:286adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/EntryHandler.java:52
Contre-vérification — Redémontré : les écritures de reprise/ajustement n'existent que comme natures sur des écritures manuelles (UI manual-entries d'adb-ui ; dispatch RentalContract.java:1249). ManualEntryEvent.java:110 fixe en dur eventType=MANUAL_ENTRY à la source ; EntryHandler.java:52 le recopie sur les écritures persistées (la nature va dans le champ distinct eventNature, jamais lu par le code du CRG). L'agrégation du CRG (CRGPeriodCustomRepositoryImpl:267/365) projette l'eventType brut ; CRGPeriodRecapFactoryImpl.java:286 écarte toute écriture dont l'eventType n'est ni null ni dans MISCELLANEOUS_EVENT_TYPES, et CRGRentalContractEntriesProjection amountByEventType (~:264) renvoie null pour MANUAL_ENTRY. Des 15 types listés, seul CALL_FOR_DEPOSIT est réellement persisté. Atténuation : les montants comptabilisés avant une période remontent dans la ligne d'ouverture « Balance » (qualifiant TENANT_CRG, AccountHelper.java:19-22), donc les totaux du grand livre finissent par se réconcilier — sévérité élevée, pas critique.
Ce constat ne paraît pas exact. Les valeurs listées, dont ACCOUNTING_TAKEOVER_TENANT_BALANCE_DEBITOR, CREDITOR_NOT_CASH et CREDITOR_CASH, sont bien des membres d'AccountingEventType : les comparer à eventType est donc attendu.
Quand on reprend un bail dont la révision de loyer n'est pas annuelle (par exemple un bail commercial révisé tous les trois ans), le système reconstitue l'historique d'indexation en supposant une révision chaque année. La périodicité de révision saisie, pourtant obligatoire pour les reprises commerciales, est ignorée. Le système fabrique donc des révisions intermédiaires qui n'ont jamais eu lieu contractuellement : l'indice de référence du bail est faux et la prochaine révision réelle sera calculée sur une mauvaise base, donc un loyer révisé erroné.
TakeOverContractIndexGenerator code en dur des révisions annuelles et ignore indexFrequency (obligatoire pour les reprises commerciales) ; fenêtre de requête ancrée sur LocalDate.now()
Le générateur reconstruit l'historique d'indexation en supposant une révision par an : il incrémente `forecastNextReviewDate.plusYears(1)` et `++nextIndexYear` à chaque indice trouvé, fabriquant des indices CURRENT effectifs pour chaque année intermédiaire. IndexFrequencyRule rend indexFrequency obligatoire pour les contrats commerciaux repris, pourtant aucun code ne lit jamais getIndexFrequency() (grep : seuls le champ, l'entité et le modèle UI le référencent). Scénario : bail commercial à indexation ICC triennale, dernière révision au 2023-06-01 (indice de base T1 2023), prochaine révision au 2026-06-01 → le générateur marque T1 2024 et T1 2025 comme révisions CURRENT appliquées qui n'ont contractuellement jamais eu lieu, l'indice courant du contrat est donc faux et la révision 2026 ne calcule qu'une année de variation au lieu de trois — sous-indexant le loyer du propriétaire. De plus, la fenêtre de requête des utilities utilise LocalDate.now().withYear(...) (ligne 43), rendant les indices générés dépendants du jour où le contrat est validé.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/indices/impl/TakeOverContractIndexGenerator.java:43adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/indices/impl/TakeOverContractIndexGenerator.java:80adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/IndexFrequencyRule.java:16
Contre-vérification — Redémontré : TakeOverContractIndexGenerator.java:63-81 incrémente les révisions strictement à l'année (plusYears(1), ++nextIndexYear), marquant les années intermédiaires CURRENT/effectives (lignes 131-144) ; le cas de test 1 prouve que des écarts pluriannuels sont acceptés en entrée. IndexFrequencyRule.java:16-21 impose indexFrequency pour les reprises commerciales, pourtant aucun code ne consomme getIndexFrequency() ; adb-ui code en dur FREQUENCY.YEARLY (contracts.service.ts:224,661) alors que la date de prochaine révision est librement éditable (rc-conditions.component.ts:912 ne fait que défaut à +1 an), et aucun validateur ne borne futureRentReviewDate. Le CURRENT fabriqué devient la base de defineCurrentIndex() dans RentalContract.applyIndex (RentalContract.java:1533-1562), sous-indexant les révisions triennales. Sévérité moyenne maintenue : erreur monétaire, mais le produit ne gère que la fréquence annuelle.
Ne paraît pas valide. indexFrequency est un champ hérité/déprécié d'une implémentation antérieure, alors que TakeOverContractIndexGenerator relève du nouveau flux de révision annuelle. La plage de dates élargie basée sur LocalDate.now() est également intentionnelle : dateFrom(LocalDate.now().withYear(takeOverKey.getYear() - 1)) / dateTo(LocalDate.now().plusYears(1)). Elle sert à récupérer un ensemble d'indices suffisamment large, car l'année d'indice nécessaire au recalcul ne correspond pas toujours à l'année courante.
Pour les écritures de reprise de solde, l'écran impose une cohérence entre le type d'opération et le sens (le locataire nous doit / nous devons au locataire), mais le serveur ne vérifie rien : il enregistre le sens tel quel dans la fiche de l'opération tout en passant l'écriture comptable selon un schéma fixe. Un autre logiciel ou une saisie hors écran standard peut donc créer une opération dont la fiche dit « à payer au locataire » alors que la comptabilité enregistre une créance sur le locataire. Ces incohérences compliquent tout contrôle ou audit ultérieur, même si la comptabilité elle-même reste juste.
Le backend ne valide ni ne respecte le sens (direction) pour les événements de reprise — l'enregistrement de l'événement peut contredire le grand livre
Les règles UI documentées contraignent le sens (DEBITOR=RECEIVABLE uniquement ; les 4 événements creditor/dépôt=PAYABLE uniquement) et la doc soulève comme question ouverte si le backend valide. Il ne le fait pas : RentalContract.manualEntry transmet event.getDirection() à toutes les autres natures manuelles mais l'omet pour les six natures de reprise locataire (débit/crédit fixes), et RentalDelegateContract fait de même pour ACCOUNTING_TAKEOVER_OWNER_BALANCE_CREDITOR. ManualEntryEvent se contente d'exiger un sens non nul. Scénario : un client d'API (p. ex. le nouveau frontend adb-web) poste une reprise DEBITOR avec direction=TENANT_PAYABLE — acceptée, le grand livre comptabilise une créance (débit 411100) alors que le AccountingEventSource.direction persisté indique payable ; tout écran/rapport indexé sur le sens de l'événement est en désaccord avec le grand livre, et la combinaison invalide n'est jamais rejetée.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1249adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.java:298adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEvent.java:220adb-doc/docs/concepts/accounting/takeover-ui-rules.md:58
Contre-vérification — RentPostingRuleFactory.java:294-402 : les 7 règles de reprise omettent le sens et comptabilisent un débit/crédit fixe ; RentalContract.java:1249-1282 et RentalDelegateContract.java:298-304 écartent event.getDirection() pour les seules natures de reprise. ManualEntryEvent.java:220-223 valide uniquement que le sens est non nul, tout en le persistant (:108), et le mapper le répercute (AbstractAccountingEventMapper:453). Aucun contrôle (nature×sens) n'existe où que ce soit dans adb-accounting. adb-ui entry-details.component.ts:492-516 applique la contrainte côté client uniquement, conformément à takeover-ui-rules.md. Une reprise DEBITOR avec TENANT_PAYABLE est acceptée, stockée et contredit le grand livre. Le grand livre lui-même reste correct, d'où sévérité moyenne.
Je ne vois pas de problème confirmé ici. Les valeurs acceptées sont représentées par CatalogValue : seules des valeurs de catalogue existantes peuvent être sélectionnées pour les champs concernés.
Aucun contrôle n'empêche de saisir une date de reprise dans le futur, antérieure au début du bail, ou une date de dernière révision postérieure à la reprise. Une simple faute de frappe (par exemple 2020 au lieu de 2023) est acceptée : le système génère alors des appels de loyer pour des périodes où le bail n'existait pas encore, et à l'inverse une date future bloque silencieusement tout quittancement. La reconstitution de l'historique d'indexation part elle aussi de cette date erronée. La correction repose entièrement sur la vigilance du gestionnaire.
Dates de reprise acceptées dans le futur ou avant le début du contrat ; l'appel de loyer initial n'est vérifié que par rapport à takeOverDate
TakeOverDateRule et TakeOverLastRentReviewDateRule ne vérifient que le caractère non nul. Rien n'impose takeOverDate >= début du contrat, takeOverDate non située dans le futur, ni takeOverLastRentReviewDate <= takeOverDate. InitialCallForRentDateRule ne rejette que initialCallForRentDate < takeOverDate. Scénario : l'utilisateur saisit par erreur takeOverDate 2020-05-01 sur un bail débutant le 2023-01-01, fixe initialCallForRentDate au 2021-01-01 — le contrat est validé, et comme firstDayOfPeriod() pour une reprise part de initialCallForRentDate (RentalContract.java:951), des appels de loyer sont générés pour des périodes antérieures à l'existence même du bail ; à l'inverse, une saisie erronée dans le futur bloque silencieusement tous les appels de loyer. Le générateur d'indices ancre ensuite le whenEffective de l'indice TAKE_OVER sur le takeOverLastRentReviewDate erroné.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/TakeOverDateRule.java:16adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/InitialCallForRentDateRule.java:17adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:951
Contre-vérification — Redémontré depuis le code : TakeOverDateRule.java:16 et TakeOverLastRentReviewDateRule.java:16 ne vérifient que le non-null ; InitialCallForRentDateRule.java:17-19 est le seul contrôle inter-champs ; aucune règle côté serveur n'impose begin<=takeOverDate ni lastRentReview<=takeOverDate. RentalContract.java:946-951 ancre le début des appels de loyer sur initialCallForRentDate/takeOverDate ; TakeOverContractIndexGenerator.java:64-71 l'ancre sur takeOverLastRentReviewDate. adb-ui (rc-conditions.component.ts:878-947) calcule des indicateurs de cohérence mais ils ne pilotent que le CSS/le texte d'erreur, pas la validité du formulaire ni le verrou de soumission de l'étape 4 — pas un vrai garde-fou. Légère surinterprétation : interdire une takeOverDate future serait erroné (les reprises à effet futur sont légitimes). Sévérité faible correcte (nécessite une faute de saisie, récupérable).
Ce constat ne tient pas compte du flux de validation complet. Les dates sont validées côté UI, et la création du contrat exécute également une liste dédiée de règles de validation backend.
Toute reprise de solde locataire doit obligatoirement être datée au 1er du mois de l'appel de loyer concerné. Une reprise effective en milieu de mois (par exemple le 15 mars) est donc comptabilisée au 1er mars — avant même le début du mandat — ou au 1er avril. La comptabilité, et demain le fichier FEC, ne peuvent pas porter la vraie date du transfert de solde, et pour une reprise avec espèces remises, l'écriture de banque tombe un jour où aucun fonds n'a bougé. Cela complique le rapprochement bancaire et la traçabilité de la reprise en cas de contrôle.
Les écritures de reprise locataire forcent la postingDate au 1er du mois de l'appel de loyer, perdant la véritable date de reprise
Tous les événements de reprise côté locataire portent le sens TENANT_RECEIVABLE/TENANT_PAYABLE, si bien que ManualEntryEvent exige un callForRentPeriod et isValidPostingDate rejette toute date de comptabilisation autre que le jour 1 de ce mois. Une reprise prenant effet en milieu de mois (p. ex. 2026-03-15) doit être comptabilisée avec PostingDate 2026-03-01 (avant le début du mandat) ou 2026-04-01 — le grand livre et le futur champ FEC 4 (PostingDate) ne peuvent pas porter la vraie date de transfert du solde, et le débit bancaire CREDITOR_CASH est daté d'un jour où aucun fonds n'a bougé, compliquant le rapprochement bancaire et la piste d'audit de la reprise.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEvent.java:238
Contre-vérification — Redémontré : l'UI restreint les natures de reprise locataire à TENANT_RECEIVABLE/TENANT_PAYABLE (entry-details.component.ts:502-510) et aux stratégies CFR/CFR_ADJUSTMENT (:518-519). Côté backend, hasCallForRentPeriod (ManualEntryEvent.java:230-236) impose la période pour ces sens (la stratégie de getters par défaut d'Hibernate Validator inclut les has*-boolean, donc elle s'exécute sous @Validated dans AccountingEventController.java:51/71), et isValidPostingDate (:238-246) force postingDate=period.atDay(1) ; aucune exemption pour les reprises. Le grand livre hérite de event.getPostingDate() (RentalContract.java:749-758, branche par défaut), et CREDITOR_CASH débite la banque 512101 (RentPostingRuleFactory.java:342-356 ; doc accounting-takeover-tenant-balance-creditor-cash.md). Le chemin à date libre CFR_ADJUSTMENT est rejeté côté serveur (aucune période envoyée). Sévérité faible correcte : montants/soldes justes ; seule la fidélité de la date/le rapprochement est en jeu.
Je ne suis pas d'accord avec ce constat. La couche de validation impose la relation autorisée entre postingDate et callForRentPeriod : une écriture de reprise incohérente ne passerait pas la validation.
Quand un encaissement déjà comptabilisé est annulé (par exemple un prélèvement rejeté), la dette du locataire ou la facture redevient bien due dans les comptes. Mais l'écran de recherche des écritures non lettrées la calcule mal : il additionne le paiement et son annulation au lieu de les compenser, et considère donc la dette comme réglée, voire trop réglée. Résultat, la ligne disparaît des listes d'impayés et du lettrage manuel. Des loyers ou factures redevenus dus peuvent ainsi passer sous les radars des équipes de recouvrement.
La recherche des écritures auxiliaires additionne les lettrages sans tenir compte du sens : les dettes rouvertes par la contre-passation d'un paiement disparaissent des recherches de non-lettrés
AuxiliaryAccountAggregationOperations calcule settledAmount en additionnant CHAQUE montant de lettrage, sans tenir compte du sens, alors que le modèle Java (AccountingEntry.settledAmount) et l'agrégation équivalente dans DomainObjectEntryReactiveRepository.findAuxiliaryAccountEntries calculent crédit-moins-débit. Lorsqu'un paiement locataire/fournisseur comptabilisé est contre-passé, ReversalSettlementsHandler ajoute des lettrages REVERSE de sens opposé à l'écriture de débit de l'appel de loyer/facture lettrée : le montant ouvert Java revient correctement au montant intégral, mais l'agrégation compte les deux lettrages (ex. 700 CREDIT + 700 DEBIT = 1400 lettrés), si bien que unsettledAmount devient négatif et l'écriture est exclue par les filtres gt(0)/includeOnlyUnSettled. Concrètement : un loyer de 700 EUR lettré par un prélèvement ensuite rejeté et contre-passé -> la créance de 700 EUR rouverte n'apparaît plus dans aucun écran/recherche de « non-lettrés » (AccountingEntryServiceImpl.entriesSearch), donc elle n'est jamais re-lettrée ni relancée.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/aggregations/impl/AuxiliaryAccountAggregationOperations.java:93adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/DomainObjectEntryReactiveRepository.java:178adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/AccountingEntry.java:312
Contre-vérification — Re-dérivé : un lettrage normal enregistre un CREDIT sur l'écriture de DÉBIT de l'appel de loyer (SettlementServiceImpl.java:376) ; la contre-passation du paiement y ajoute un lettrage DÉBIT de sens opposé (AccountingEntry.java:293-294 via ReversalSettlementsHandler.java:69, persisté ligne 80). Java se compense à 0 lettré/700 ouvert (AccountingEntry.java:312-327), mais AuxiliaryAccountAggregationOperations.java:112-116 additionne les deux sens (1400), donc unSettledAmount=-700 et les filtres gt(0.01)/gt(min) (lignes 64-68) écartent la dette rouverte. Cette agrégation alimente searchAuxiliaryAccountEntries/count utilisés par AccountingEntryServiceImpl.java:228-234, et l'UI envoie unsettledAmountMin:0 (adb-ui payment.service.ts:436). L'équivalent tenant compte du sens (DomainObjectEntryReactiveRepository.java:180-183) confirme l'intention. La gravité haute est correcte : les soldes du grand livre restent justes ; les recherches/comptages de non-lettrés perdent silencieusement la dette.
Le reversement automatique du loyer encaissé vers le compte du propriétaire est censé se déclencher uniquement pour de vrais encaissements. Or le filtre prévu pour exclure certaines écritures manuelles (reprises de solde lors d'une reprise de gestion, dépôts de garantie) ne fonctionne pas : toute écriture manuelle au crédit qui solde un appel de loyer déclenche le reversement. Concrètement, un avoir manuel ou une reprise de solde « locataire créditeur sans trésorerie » peut créditer le propriétaire alors qu'aucun argent n'a été encaissé. Risque direct de reversements indus et de comptes propriétaires faux.
Le transfert au propriétaire (TENANT_TO_OWNER, 800000 -> 466100) se déclenche pour TOUT crédit manuel : la liste blanche des natures de reprise est du code mort
PostSettlementsProcessorImpl.isEligibleEntry renvoie `isAllowedEventType || isAllowedManualEntry`, mais MANUAL_ENTRY figure lui-même dans ALLOWED_CREDIT_EVENT_TYPE, donc la première condition est vraie pour tout crédit MANUAL_ENTRY et la liste blanche des natures (ALLOWED_POST_SETTLEMENT_NATURES, qui exclut délibérément ACCOUNTING_TAKEOVER_TENANT_BALANCE_CREDITOR_NOT_CASH et TENANT_DEPOSIT) ne restreint jamais rien. Les écritures de reprise sont comptabilisées en MANUAL_ENTRY avec un eventNature (ManualEntryEvent.java:109-110). Concrètement : un crédit de reprise « créditeur non encaissé » (ou tout avoir OD manuel / écriture de remise de loyer, 467200 -> 411100) lettre un débit CALL_FOR_RENT ouvert -> CallForRent.postSettlement émet TenantToOwner -> le compte propriétaire 466100 est crédité pour de l'argent que l'agence n'a jamais reçu, et le propriétaire est reversé au prochain compte rendu de gestion (CRG).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:201adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForRent.java:100adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEvent.java:109
Contre-vérification — Re-dérivé : PostSettlementsProcessorImpl.java:49-53 inclut MANUAL_ENTRY dans ALLOWED_CREDIT_EVENT_TYPE, donc isEligibleEntry (:201-206) laisse passer tout crédit MANUAL_ENTRY et la liste blanche des natures (:54-60, excluant CREDITOR_NOT_CASH/dépôt) est inatteignable — même logique avant re-sync, pas une régression. ManualEntryEvent.java:110 comptabilise les reprises en MANUAL_ENTRY ; AccountingFunctions.java:145+202 déclenche postProcess à toute mise à jour de lettrage en ne vérifiant que le côté débit ; CallForRent.java:114-124 émet alors TenantToOwner (Dr 800000 / Cr 466100 selon la doc TENANT_TO_OWNER) sans condition pour les crédits non-GLI, en contradiction avec la règle documentée « pas d'écriture propriétaire après lettrage » (accounting-takeover-tenant-balance-creditor-not-cash.md:20). La gravité haute est maintenue.
Quand un loyer est encaissé par prélèvement SEPA, l'encaissement est bien enregistré en banque, mais le rapprochement automatique avec l'appel de loyer n'est jamais fait : le mécanisme prévu pour cela n'est en réalité jamais utilisé. L'appel de loyer reste donc affiché comme impayé et le reversement au propriétaire ne se déclenche pas, tant qu'un gestionnaire ne fait pas le lettrage à la main. Pour un parc géré principalement en prélèvement, cela crée une charge manuelle systématique et retarde les reversements aux propriétaires.
Les paiements par prélèvement SEPA ne sont jamais lettrés automatiquement : le chemin de lettrage DIRECT_DEBIT est du code mort inatteignable
DirectDebitPaymentCreationStrategy construit le paiement avec une unique PaymentShare dont la cible est le mandat SEPA (descriptor.getSepa()), mais PaymentServiceImpl.handleShare ne crée une source de lettrage que si le type de cible de la share == TYPE_INVOICE ; la méthode privée getDirectDebitPaymentSettlementSource (le seul endroit où une SettlementCreationSource DIRECT_DEBIT_PAYMENT est construite) n'a aucun appelant, donc DirectDebitPaymentSettlementCreationStrategy enregistrée dans la factory est inatteignable. Concrètement : un prélèvement SEPA mensuel de loyer est comptabilisé (512101 -> 411100/RC) mais ne lettre jamais les débits CALL_FOR_RENT ouverts, donc TENANT_TO_OWNER ne se déclenche jamais et le propriétaire n'est pas crédité tant qu'un utilisateur ne lettre pas manuellement via SettlementController ; la share reste paid=false, si bien que chaque message SQS onPaymentModified relance payPaymentSettlements sans rien faire, indéfiniment. De plus, la stratégie morte elle-même imputerait dans l'ordre naturel Mongo (la requête n'a pas de tri ; CriteriaHelper.sorting n'ajoute rien quand pageable.sorts est null) et son filtre `!isReversedEntry()` exclut définitivement toute dette ayant déjà eu un lettrage contre-passé.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:327adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/creation/impl/DirectDebitPaymentCreationStrategy.java:83adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/settlement/creation/impl/DirectDebitPaymentSettlementCreationStrategy.java:29
Contre-vérification — Re-dérivé indépendamment. DirectDebitPaymentCreationStrategy.java:83-88 construit l'unique share ciblant descriptor.getSepa() (type SEPA_MANDATE_CONTRACT, jamais « Invoice »). PaymentServiceImpl.handleShare:327-329 ne crée une source de lettrage que pour TargetObject.TYPE_INVOICE, sans branche else, donc les shares DD tombent toujours sur defaultIfEmpty et restent paid=false. getDirectDebitPaymentSettlementSource (PaymentServiceImpl.java:353) n'a aucun appelant dans tout le dépôt ; seules les origines MANUAL, INVOICE_PAYMENT et cette origine DIRECT_DEBIT_PAYMENT morte construisent une SettlementCreationSource, donc la stratégie enregistrée dans la factory est inatteignable hors tests. TENANT_TO_OWNER n'est émis que depuis CallForRent.postSettlement (CallForRent.java:100-121) via PostSettlementsProcessor, donc le crédit propriétaire nécessite un lettrage manuel (SettlementController, l'origine MANUAL fonctionne). La sous-affirmation isReversedEntry est aussi exacte (AccountingEntry.java:201-205). La gravité haute est maintenue : flux central du loyer SEPA, mais un contournement manuel existe et les écritures elles-mêmes sont correctes.
Quand un utilisateur règle plusieurs factures avec un seul paiement en indiquant combien affecter à chacune, la comptabilité n'applique pas cette répartition : la première facture absorbe la totalité du paiement, et le rapprochement des suivantes échoue. Les fiches des factures affichent pourtant la répartition voulue par l'utilisateur. Fiches factures et comptabilité racontent donc durablement deux histoires différentes sur ce qui est payé, ce qui fausse le suivi fournisseurs et complique tout contrôle ou audit.
Le lettrage d'un paiement de facture ignore l'imputation par facture saisie par l'utilisateur et les montants des PaymentShare ; les montants lettrés au niveau facture et au niveau grand livre divergent
AllocationHelper.allocateCreditToDebits initialise `remainingCredit = credit.getAmount()` — le montant INTÉGRAL du paiement — et PaymentShare.amount / InvoicePaymentAllocation.allocated ne sont jamais transmis à la chaîne de lettrage. Concrètement : l'utilisateur paie un paiement fournisseur de 300 en imputant 100 à la facture A (ouvert 300) et 200 à la facture B (ouvert 200). La share A lettre les écritures de débit de A pour min(300,300)=300 (et non 100) ; la share B échoue alors sur validateTotalSettlements (« Settling entry must have a positive open amount ») et B reste ouverte — alors qu'enrichInvoices a inscrit InvoiceSettledAmount 100 sur A et 200 sur B à partir des shares. Les documents de facture et les écritures comptables sont désormais définitivement en désaccord sur ce qui est payé. De plus, les shares sont traitées en flatMap concurrent (payPaymentSettlements) et chaque lettrage relit indépendamment le montant ouvert du crédit, si bien que des shares concurrentes peuvent imputer deux fois les mêmes euros ; aucune invariance ne vérifie nulle part sum(shares)==payment.amount.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/AllocationHelper.java:20adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:154adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/payment/impl/AbstractInvoicePaymentRepositoryStrategy.java:91
Contre-vérification — Re-dérivé depuis le code : AllocationHelper.java:20 initialise le montant intégral du crédit (ni openAmount, ni le montant de la share) ; PaymentShare.amount n'atteint jamais la chaîne de lettrage (PaymentServiceImpl.java:338-351, SettlementsHelper.java:26, InvoicePaymentSettlementCreationStrategy.java:37-41). L'imputation partielle par facture est légale (InvoicePaymentServiceImpl.java:149 ne vérifie que allocated<=open) et les paiements fournisseurs multi-factures créent des paiements multi-shares (AbstractInvoicePaymentRepositoryStrategy.java:120-128). Scénario vérifié : la share A sur-lettre par rapport à l'ouvert de la facture A (300 vs 100 désigné) ; la share B lève une erreur à SettlementServiceImpl.java:238. Les documents inscrits avec les montants des shares de façon synchrone (AbstractInvoicePaymentRepositoryStrategy.java:97-99) divergent des lettrages du grand livre asynchrones (PaymentFunctions.java:75-84). Seuls les paiements en imputation intégrale fonctionnent par coïncidence. Gravité haute correcte.
Le contrôle qui vérifie qu'on n'affecte pas plus que le montant disponible d'un paiement compare des montants arrondis à l'euro : les centimes sont perdus. Dans certains cas, on peut donc affecter quelques centimes de plus que le montant réellement disponible ; si deux affectations sont traitées en même temps, elles peuvent toutes deux passer et le paiement se retrouve sur-lettré, avec un solde négatif. L'écart reste de l'ordre de centimes par opération, mais il fausse les soldes et complique les rapprochements bancaires.
La validation du total de lettrage tronque les centimes (intValue), permettant un sur-lettrage de l'écriture payeuse
validateTotalSettlements additionne les montants de lettrage et les compare au montant ouvert du crédit via BigDecimal.intValue(), qui supprime tous les centimes (et déborderait au-delà de 2^31). Concrètement : crédit ouvert = 100,50 EUR, lettrages demandés 50,30 + 50,30 = 100,60 EUR ; la comparaison entière 100 <= 100 passe. handleSettlementProcess plafonne ensuite chaque lettrage à min(demandé, créditOuvert-à-la-lecture, débitOuvert), mais le flux de lettrages est traité en flatMap (souscriptions internes concurrentes) donc les deux peuvent lire créditOuvert=100,50 avant qu'aucune sauvegarde ne soit persistée — total lettré 100,60 > 100,50 et le montant ouvert de l'écriture de paiement devient négatif (affiché comme sur-lettré). La validation par lettrage (validateSettlement) compare correctement le Money intégral au débit, donc seul le côté crédit est exposé.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/SettlementServiceImpl.java:241
Contre-vérification — SettlementServiceImpl.java:241-244 valide le total de lettrage via BigDecimal.intValue(), tronquant les centimes (50,30+50,30 vs ouvert 100,50 passe comme 100<=100). Le côté crédit n'a pas de garde exacte (validateSettlement:256 et validateSettledAmounts:398 ne portent que sur le débit). settlementsProcess:113 fait un flatMap concurrent, chaque branche re-récupérant l'écriture de crédit (lignes 114-115) avant toute sauvegarde, donc le plafonnement min() utilise des montants ouverts périmés ; les sauvegardes sont des $push Mongo (DomainObjectEntryRepositoryImpl.java:103) dont la garde d'idempotence est inerte (RequestContextData.idempotencyKey jamais renseigné, RequestContextWebFilter). Atteignable via SettlementController.java:57. Le openAmount du crédit devient négatif. Gravité moyenne confirmée.
Quand le système propose d'affecter un paiement aux dettes ouvertes d'un locataire, il commence par la dette la plus récente au lieu de la plus ancienne. Un locataire qui doit douze mois de loyer et en paie un voit ainsi son dernier loyer soldé tandis que le plus ancien continue de vieillir vers la prescription de trois ans, et les relances visent la mauvaise période. En droit français, à défaut d'indication du débiteur, l'imputation doit se faire sur la dette la plus ancienne. En outre, au-delà de cent lignes ouvertes, les dettes les plus anciennes ne sont même plus prises en compte dans la proposition.
La proposition de lettrage impute les dettes les plus récentes d'abord et écarte silencieusement les arriérés au-delà des 100 écritures les plus récentes
handleSettlementPropose trie les débits ouverts candidats par priorité de lettrage puis par `postingDate` en Comparator.reverseOrder() — l'appel de loyer le PLUS RÉCENT est lettré en premier, laissant les arriérés les plus anciens ouverts. De plus, la liste de candidats elle-même provient de findAuxiliaryAccountEntries avec `$sort {_id:-1}` et une pagination fixe (0, 100), donc pour un locataire ayant plus de 100 écritures de débit ouvertes, les dettes les plus anciennes ne sont même pas candidates. Concrètement : un locataire doit les loyers de janvier à décembre, paie un mois : l'appel de décembre est lettré, janvier reste ouvert et continue de vieillir vers la prescription triennale tandis que la relance vise la mauvaise période. En droit français, l'imputation par défaut porte sur la dette la plus ancienne lorsque le débiteur n'en désigne pas. Aucun test n'atteste de cet ordre (SettlementServiceImplTest ne couvre que les flux vides).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/SettlementServiceImpl.java:386adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/SettlementServiceImpl.java:286adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/DomainObjectEntryReactiveRepository.java:203
Contre-vérification — Re-dérivé : SettlementServiceImpl.java:386-396 trie les candidats par priorité de type d'événement puis par postingDate DESC (AccountingEventType.java : dépôt=1, loyer=2), donc au sein d'une classe l'appel de loyer le plus récent est imputé en premier ; getTargetOpenEntries (:305-306) passe (0,100) à findAuxiliaryAccountEntries dont le pipeline trie {_id:-1} puis $skip/$limit (DomainObjectEntryReactiveRepository.java:202-204), excluant les débits ouverts au-delà des 100 plus récents. SettlementServiceImplTest.java:89 ne teste que le flux vide. La règle légale (art. 1342-10 al.2 C.civ., prescription triennale des loyers) est correctement énoncée. Mitigation le maintenant en moyen : propose utilise la stratégie DBLess (rien n'est persisté) et recieve-payment-modal.component.ts permet aux opérateurs de modifier les imputations ligne par ligne avant /process — mais le modal n'affiche que les 100 écritures proposées et les valeurs par défaut sont ce que les opérateurs acceptent ; le chemin automatique du prélèvement utilise un autre allocateur et n'est pas affecté.
Après annulation d'un paiement, le paiement d'origine garde son statut « comptabilisé » : rien dans son statut n'indique qu'il a été annulé, seul un lien discret vers l'écriture d'annulation le montre. Comme ce lien est enregistré avec un léger décalage, deux demandes d'annulation quasi simultanées peuvent toutes deux aboutir — le locataire serait alors remboursé deux fois. Par ailleurs, un paiement dont la comptabilisation échoue reste bloqué en erreur sans possibilité de relance. Le risque de double annulation reste limité au cas de deux demandes très rapprochées.
La machine à états des paiements est bloquée : REVERSED/ARCHIVED inatteignables, ERROR terminal, le paiement d'origine reste BOOKED après contre-passation, course à la double contre-passation
Les transitions BOOKED->REVERSED et REVERSED->ARCHIVED sont câblées sur UnsupportedOperationAction (toujours Mono.error) et Payment.Transition.REVERSE n'est jamais envoyée par aucun code, donc aucun paiement n'atteint jamais REVERSED/ARCHIVED ; un paiement contre-passé reste paymentState=BOOKED et n'est repérable que via un lien relatedPayments REVERSAL. Ce lien est écrit de façon asynchrone après la comptabilisation du paiement de contre-passation (processReverse -> addRelatedPayment), donc deux demandes de contre-passation concurrentes passent toutes deux validatePaymentForReversal et produisent deux paiements de contre-passation (locataire remboursé deux fois). ERROR (positionné par BookPaymentAction.handleFailureWithFallback) n'a aucune transition sortante ni mécanisme de réessai ; combiné au fait qu'asyncPaymentBooking est une souscription différée fire-and-forget (perdue au redémarrage du pod), un paiement SEPA dont la comptabilisation automatique échoue ou est interrompue reste PARKED/ERROR pour toujours et le loyer reçu n'est jamais comptabilisé.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:590adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/payment/actions/BookPaymentAction.java:44adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/payment/actions/UnsupportedOperationAction.java:25
Contre-vérification — Tient partiellement. Réfuté : la course au double remboursement — les événements de contre-passation partagent une clé déterministe (ReverseAccountingEvent.java:140-151), UniquenessHandler s'exécute dans la chaîne de contre-passation (HandlerChainConfig), et un index unique partiel sur accounting_event.unique (adb-infrastructure/mongodb-atlas/data-scripts/events_unique_constraint.js) bloque une seconde contre-passation PROCESSED ; le second paiement atterrit en ERROR (AbstractPaymentStrategy.java:40), pas comptabilisé deux fois. Confirmé : REVERSED/ARCHIVED inatteignables (PaymentServiceImpl.java:607-618, REVERSE jamais envoyée) ; ERROR est terminal (BookPaymentAction.java:44, pas de transition sortante, garde PARKED-only AbstractProcessPaymentAction.java:35) ; asyncPaymentBooking perdu au redémarrage (PaymentServiceImpl.java:446-457). Mais PARKED est comptabilisable manuellement via POST /{id}/BOOK (PaymentController.java:76), donc la gravité redescend à basse.
Le système est censé détecter qu'un même paiement est soumis deux fois et n'en enregistrer qu'un. Mais pour tous les paiements hors prélèvement SEPA, la clé qui sert à repérer les doublons est fabriquée à partir de l'heure exacte de la saisie : chaque tentative obtient une clé différente, et la protection ne détecte donc jamais rien. Un double clic dans l'écran de saisie ou une requête rejouée crée deux encaissements et deux écritures de banque pour le même argent, à corriger ensuite manuellement. Seuls les prélèvements SEPA sont réellement protégés.
La clé d'idempotence des paiements hors prélèvement est en millisecondes epoch : la garde anti-doublon est inopérante
Payment.key() pour toute origine autre que DIRECT_DEBIT génère la clé d'unicité à partir de `new Date().toInstant().toEpochMilli()`, donc chaque tentative de création obtient une clé fraîche et la récupération index-unique/DuplicateKeyException dans saveIfNotExists ne peut jamais détecter un rejeu. Concrètement : un paiement locataire manuel soumis deux fois (double-clic UI, réessai HTTP ou message rejoué) crée deux paiements BOOKED et deux écritures 512101->411100 pour le même encaissement. Le CLAUDE.md du module liste explicitement la « stratégie d'idempotence des paiements rejoués » comme non documentée ; seuls les paiements DIRECT_DEBIT ont une clé déterministe (origine+sens+source+cible+sepa+période).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Payment.java:282adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:413
Contre-vérification — Re-dérivé : Payment.java:282-287 construit la clé à partir de new Date() en ms epoch pour toute origine sauf DIRECT_DEBIT ; ManualPaymentCreationStrategy.java:20 l'inscrit comme `unique` à la création (MANUAL et INSURANCE pointent tous deux vers cette stratégie, PaymentCreationStrategyFactoryImpl.java:25-26), donc PaymentServiceImpl.saveIfNotExists:413-424 ne peut jamais voir de DuplicateKeyException pour les rejeux. Aucune autre garde n'existe : le POST de PaymentController n'a pas de jeton d'idempotence, et le savePayment d'adb-ui (accounting-payment-form.component.ts:420-431) enchaîne la création puis un BOOK immédiat, donc une course au double-clic produit deux paiements/écritures BOOKED. Les contre-passations SONT gardées (validatePaymentForReversal) et DIRECT_DEBIT a une clé déterministe, ce qui limite l'exposition ; les doublons sont visibles par l'opérateur et réversibles, donc la gravité basse est maintenue.
Le système permet de créer et de consulter des exercices comptables, mais aucune fonction ne permet de les clôturer, ni eux ni leurs périodes. Concrètement, tous les exercices restent ouverts pour toujours : on peut passer des écritures antidatées dans n'importe quel exercice passé, sans limite. Les écritures de clôture, les à-nouveaux (reports de soldes locataires, propriétaires, dépôts de garantie) et l'affectation du résultat ne sont jamais générés. En l'état, un administrateur de biens ne peut pas arrêter légalement ses comptes annuels avec cet outil.
Les exercices et périodes ne peuvent jamais être clôturés ; ni à-nouveaux, ni report du résultat
FiscalYearController n'expose que GET et POST-create. FiscalYearServiceImpl comporte exactement trois méthodes (list, lookup, create) ; fiscalYearRepository.save n'est appelé que depuis create. Nulle part dans adb-accounting le code ne positionne FiscalYear.open=false ou FiscalYearPeriod.open=false, et les recherches sur la logique de report/à-nouveaux/clôture ne renvoient rien. Conséquence : chaque exercice et chaque période reste ouvert indéfiniment (sauf édition manuelle dans Mongo), les utilisateurs peuvent antidater des écritures dans n'importe quel exercice passé sans limite, les écritures de clôture et les soldes d'ouverture pour 411100/466100/165200 etc. ne sont jamais générés, et la chaîne 800000 « Résultat » n'est jamais soldée vers un compte de résultat. Un administrateur de biens ne peut pas légalement clôturer les comptes 2025 produits par ce système.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/FiscalYearController.java:35adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/FiscalYearServiceImpl.java:65adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/FiscalYearRepository.java:32
Contre-vérification — Re-dérivé : FiscalYearController.java:35-47 n'expose que GET/POST-create ; FiscalYearServiceImpl.java:88 est l'unique save ; aucun code n'écrit open=false (FiscalYearEntity.java:24 en lecture seule via les filtres FiscalYearRepository.java:32/48) ; aucun AccountingEventType de clôture/à-nouveaux ; EntryState{OPEN,CLOSED} est inutilisé. Le script d'exploitation refresh_fiscal_year.js insère de nouveaux exercices open:true sans clôturer les précédents, confirmant qu'aucune clôture n'existe. adb-ui ne fait que des GET sur /accounting/years. Sévérité abaissée à élevée : le garde-fou du flag open (FiscalYearHandler) fonctionne, donc la clôture reste atteignable via le canal existant de scripts de données Mongo plus des écritures MANUAL pour les à-nouveaux ; par ailleurs 800000 est un miroir de lettrage hors bilan (plan-of-accounts.md:93-99), et non une chaîne de résultat.
Limite du MVP actuel plutôt que régression inattendue. Le cycle de vie de l'exercice n'a jamais été entièrement implémenté : la seule exigence était de disposer d'un exercice pour l'année civile concernée afin que les opérations comptables puissent être traitées. Un nouvel exercice est actuellement ajouté manuellement en début d'année.
La documentation prévoit l'export FEC (le fichier des écritures comptables exigé par l'administration fiscale lors d'un contrôle), mais rien n'a été construit. Le seul export existant est un tableau type Excel qui contient les montants et les dates, mais il manque des champs obligatoires du FEC : date de validation des écritures, lettrage et informations de devise. Ces données ne sont d'ailleurs même pas enregistrées dans la base. En cas de contrôle fiscal, l'agence serait incapable de fournir un FEC conforme sans un chantier de développement conséquent, avec le risque de sanctions que cela implique.
Aucun export FEC n'existe ; les écritures persistées n'ont ni ValidDate, ni lettrage, ni champs de devise
La documentation (fec.md) spécifie le FEC à 19 champs comme cible, mais aucun code d'export n'existe : une recherche sur l'ensemble du dépôt pour FEC/EcritureNum/JournalCode/CompteNum dans les sources principales d'adb-accounting ne trouve rien. L'artefact le plus proche est l'export xlsx/Google-Sheets construit à partir de LedgerAccountingEntry, qui porte entryNumber/journal/dates/débit/crédit mais n'a ni date de validation, ni code/date de lettrage, ni champs de devise. Le document AccountingEntry lui-même ne stocke que whenOccurred/postingDate/documentDate — il n'y a aucune ValidDate à exporter ni code de lettrage au niveau de l'écriture (les lettrages existent mais ne portent aucun identifiant de lettrage). Une agence auditée ne pourrait pas produire un FEC conforme à partir de ce modèle de données sans ajouter de nouveaux champs de persistance, et pas seulement un exporteur.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/LedgerAccountingEntry.java:93adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/AccountingEntry.java:56adb-doc/docs/concepts/accounting/fec.md:1
Contre-vérification — Fond confirmé : une recherche sur tout le dépôt ne trouve aucun exporteur FEC (aucun EcritureNum/JournalCode/CompteNum/ValidDate dans les sources principales, adb-reports ne contient que des templates PDF/notification) ; LedgerAccountingEntry.java:34-105 s'arrête à débit/crédit/solde/building sans validDate/lettrage/devise ; AccountingEntry.java:56-74 n'a que whenOccurred/postingDate/documentDate ; fec.md:38-52 confirme le lettrage « pending » et le n° d'écriture « à clarifier ». Sévérité surestimée : EcritureLet/DateLet/Montantdevise/Idevise peuvent légalement rester vides (fec.md lignes 14-18, Money.java porte la devise), et ValidDate pourrait être dérivée du creationDateTime immuable dans AbstractReferencedObject.java:54 — donc le manque principal est un exporteur, pas de nouveaux champs de persistance ; l'exposition ne survient qu'au contrôle fiscal.
De mémoire, l'export FEC n'a jamais été implémenté.
Deux défauts se cumulent. D'abord, un numéro d'écriture est consommé avant les contrôles (doublon, période close, journal manquant, écriture déséquilibrée) : chaque opération rejetée brûle définitivement un numéro, ce qui garantit des trous dans la séquence. Ensuite, la numérotation redémarre au 1er janvier de chaque année civile, et non au début de l'exercice comptable. Or la réglementation exige une numérotation continue et chronologique par exercice : ces deux points fragilisent la conformité du FEC et donnent prise à contestation lors d'un contrôle fiscal. Les mêmes défauts touchent la numérotation des références de pièces.
La numérotation des écritures (EcritureNum) présente des trous par conception et se réinitialise par année civile, non par exercice
Deux problèmes se cumulent. (1) Le compteur Mongo est incrémenté dans une session client privée explicitement isolée de la transaction environnante (le commentaire du code le dit), et TransactionSequenceHandler est le 2e handler de la chaîne — avant PreValidationHandler, UniquenessHandler, les contrôles journal/exercice/période et le contrôle d'équilibre. Tout événement rejeté par un handler ultérieur (doublon, période clôturée, journal manquant, déséquilibré) consomme définitivement un numéro, garantissant des trous dans la séquence stockée sur les écritures (AccountingEntry.sequence, exporté en entryNumber). La même isolation s'applique aux compteurs de pieceReference. (2) Le compteur est indexé sur (organisationId, année civile de postingDate). FiscalYear.beginDate/endDate étant arbitraires, pour un exercice décalé (p. ex. juil.-juin) la numérotation se réinitialise à 1 en cours d'exercice et peut dupliquer des valeurs au sein du FEC d'un même exercice.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CustomSequenceRepoImpl.java:95adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/config/HandlerChainConfig.java:79adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/TransactionSequenceHandler.java:18
Contre-vérification — Les deux modes de défaillance re-dérivés. `process()` est @Transactional (AccountingEventServiceImpl.java:94) ; TransactionSequenceHandler est le 2e de la chaîne (HandlerChainConfig.java:79) et incrémente le compteur dans une session privée explicitement isolée de la transaction (CustomSequenceRepoImpl.java:90-104), si bien que les rollbacks dus aux rejets ultérieurs (FiscalYearHandler/PeriodHandler Mono.error, PreValidationHandler throws, UniquenessHandler ERROR) consomment définitivement des numéros ; sequence devient entryNumber (AccountingEntryMapper.java:54). La clé du compteur utilise postingDate.getYear() (TransactionSequenceHandler.java:18) alors que FiscalYear begin/end sont arbitraires (FiscalYear.java) — réinitialisation en cours d'exercice possible. Aucun export FEC ne renumérote ; fec.md:51 signale ce point ouvert. Le scénario de réinitialisation nécessite une organisation à exercice décalé en prod. Sévérité élevée maintenue : risque de conformité/audit, sans anomalie financière.
Les trous de séquence sont une conséquence connue de l'implémentation actuelle. L'incrément de séquence a été déplacé dans une transaction séparée pour éviter les numéros d'écriture en double lors du traitement concurrent d'événements. Si la transaction principale échoue et est annulée, la séquence reste incrémentée, ce qui crée un trou : c'est un compromis technique assumé en faveur de l'unicité. Historiquement, des trous ont aussi pu venir de rollbacks manuels en production ou d'écritures perdues après attribution de leur numéro. Concernant la remise à zéro par année civile plutôt que par exercice, cette exigence n'a pas été discutée lors de la conception initiale.
La liste des journaux comptables installée automatiquement au premier démarrage est incomplète : quatre journaux utilisés par onze types d'opérations en sont absents, notamment ceux des reprises de gestion, des passages en créances douteuses, des indemnités GLI et des transferts vers le compte propriétaire. Sur tout environnement fraîchement installé (nouveau déploiement, restauration après sinistre, base de test), ces opérations échouent avec une erreur. Comme les transferts fournisseur-vers-propriétaire et GLI-vers-propriétaire se déclenchent automatiquement lors des règlements, ce sont des chaînes entières de paiement qui se bloquent. Les environnements existants, déjà garnis, ne sont pas touchés.
Le seed journals.csv omet 4 journaux référencés par 11 types d'événements comptables
JournalServiceImpl alimente la collection globale `journals` depuis journals.csv uniquement lorsque la collection est vide. Le CSV contient 10 journaux (dont ADJUSTMENTS et PURCHASES, qu'aucun événement n'utilise) mais omet TAKEOVER_BALANCE, GLI_INDEMNITIES, SUPPLIER_TO_OWNER et GLI_TO_OWNER — clés de journal portées par les 7 événements ACCOUNTING_TAKEOVER_*, TENANT_TO_BAD_DEBT(_CLEAR), SUPPLIER_TO_OWNER et GLI_TO_OWNER. Sur tout environnement fraîchement initialisé (nouveau déploiement, restauration PRA, base d'intégration), la recherche de JournalHandler renvoie vide et l'événement échoue avec « No journal found for description[...] ». Comme SUPPLIER_TO_OWNER/GLI_TO_OWNER se déclenchent automatiquement au lettrage, les chaînes de paiement fournisseur et de lettrage GLI se cassent. La fixture de test journals.json utilise un jeu de 5 journaux entièrement différent avec d'autres codes (BUY=1 vs CSV 010), de sorte que le JournalCode du FEC ne serait même pas stable d'un environnement à l'autre.
adb-accounting/src/main/resources/journals.csv:1adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/JournalServiceImpl.java:66adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/JournalHandler.java:38adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java:125
Contre-vérification — Re-dérivé : journals.csv n'alimente que 10 journaux ; 11 constantes AccountingEventType (AccountingEventType.java:106,125-131,138-140) référencent TAKEOVER_BALANCE/GLI_INDEMNITIES/SUPPLIER_TO_OWNER/GLI_TO_OWNER ; JournalRepository.findOne fait une correspondance exacte sur description, et JournalHandler.java:36-41 renvoie l'erreur « No journal found » si vide. Le seed ne s'exécute que lorsque count()==0 (JournalServiceImpl.java:67-76) ; JournalController est en lecture seule, aucune autre voie d'écriture. La documentation (adb-doc/docs/concepts/accounting/journals.md) confirme la spécification à 13 journaux. Cependant infra/seeds/data/adb-accounting/journals.json contient déjà 12 des 13 (seul GLI_TO_OWNER manque), et les bases de prod sont peuplées — impact limité aux environnements fraîchement initialisés/à base vide, d'où sévérité moyenne. Le détail sur la fixture de test (« jeu de 5 journaux, BUY=1 ») est inexact.
Le CSV semble surtout destiné à initialiser un environnement totalement vide. Dans les environnements existants, les données de journaux les plus récentes étaient normalement maintenues par des scripts de migration de base. Les valeurs manquantes du CSV n'indiquent donc pas nécessairement un problème en production. Il faudrait vérifier que tous les journaux référencés sont bien créés par les scripts de migration ; s'il en manque là aussi, le problème toucherait surtout les déploiements neufs.
Quand un utilisateur fournit lui-même une référence de pièce, elle est acceptée telle quelle pour n'importe quelle opération : aucun contrôle d'unicité ni de format, deux écritures peuvent donc porter la même référence ou une référence fantaisiste. Quand le système génère la référence, le format réel ne correspond pas aux familles de numérotation documentées. Enfin, en mode aperçu (avant validation d'une saisie manuelle), la référence affichée est un nombre tiré au hasard, parfois négatif, qui ne correspondra jamais à celle réellement enregistrée : les aperçus utilisés pour vérifier une saisie ne sont donc pas fiables.
Le pieceReference fourni par l'utilisateur est accepté tel quel pour tout événement ; le mode propose fabrique des références aléatoires
PieceReferenceHandler utilise tel quel n'importe quel event.pieceReference non nul, pour tout type d'événement, sans aucun contrôle d'unicité, de format ou de doublon face aux écritures existantes — deux événements peuvent partager une référence ou porter des chaînes arbitraires. Lorsqu'elle est générée, la référence est « <PREFIX>_<n> » avec un compteur indépendant par type d'événement, et non les familles chronologiques documentées, partagées et complétées par des zéros CF00000001/INV/ITN (dérive doc/code). De plus, en mode propose (aperçu), la stratégie renvoie random.nextLong() — potentiellement négatif — de sorte que les utilisateurs voient des références de pièce comme « ME_-8123456789 » qui ne correspondent jamais à la référence réellement comptabilisée au process, minant la confiance dans les aperçus utilisés pour valider les écritures manuelles.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/PieceReferenceHandler.java:14adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/ProposeRepositoryStrategy.java:62adb-doc/docs/concepts/accounting/piece-reference.md:25
Contre-vérification — PieceReferenceHandler.java:13-19 utilise tel quel n'importe quel event.pieceReference non nul pour tout type d'événement ; le champ est un @JsonProperty modifiable avec seulement @Size(max=150) (AbstractAccountingEvent.java:78-81), accepté sur /accounting/events/process (AccountingEventController.java:82-90). Aucun garde-fou d'unicité/format : UniquenessHandler vérifie les clés d'événement, pas les références ; AccountingEventEntity n'a pas d'index unique. Les références générées sont « PREFIX_n » par type d'événement (CustomSequenceRepoImpl.java:70-84), et non les familles documentées CF/INV/ITN (piece-reference.md:24-30) — bien que ce document soit généré par IA avec un avertissement d'exactitude. ProposeRepositoryStrategy.java:67-69 renvoie random.nextLong() (potentiellement négatif), affiché dans les réponses d'aperçu. Sévérité moyenne maintenue : le FEC n'exige pas l'unicité du PieceRef, donc l'impact porte sur la traçabilité d'audit, non sur une corruption de comptabilisation.
Intentionnel. L'utilisateur est responsable de la valeur de pieceReference, le backend n'impose donc ni format ni unicité. En mode propose, les références aléatoires sont également intentionnelles : le flux n'est qu'une simulation destinée à afficher les résultats calculés, et générer des valeurs temporaires évite des accès inutiles à la base.
En principe, toute écriture déséquilibrée (débits différents des crédits) est rejetée. Ce garde-fou est désactivé précisément pour les transferts d'argent vers les propriétaires (locataire vers propriétaire, locataire vers agence, fournisseur vers propriétaire, GLI vers propriétaire). Aujourd'hui ces opérations produisent naturellement des écritures équilibrées, donc aucun dégât n'est constaté ; mais toute évolution future ou saisie manuelle sur ces types d'opérations pourrait enregistrer une écriture déséquilibrée en silence, faussant les comptes propriétaires sans aucune alerte. Rien n'explique ni ne justifie pourquoi ce contrôle a été retiré.
Le contrôle d'équilibre en partie double est contourné pour tous les événements TransferBalanceSheet
EntryHandler.handleResultingTransaction lève NON_ZERO_BALANCE pour les transactions déséquilibrées — sauf lorsque l'événement est un TransferBalanceSheet (TenantToOwner, TenantToAgency, SupplierToOwner, GliToOwner). Ce sont précisément les événements de transfert de fonds vers le propriétaire que la spécification équilibre via le compte miroir 800000. Aujourd'hui PostingRuleFactory.transfer() émet par hasard un débit et un crédit du même montant, donc l'exemption reste latente ; mais toute voie de règle de comptabilisation future ou manuelle pour ces types d'événements peut persister silencieusement une transaction déséquilibrée, corrompant le grand livre propriétaire 466100 et l'invariant à somme nulle 800000 sans aucun filet de sécurité. Aucun test ni commentaire ne documente pourquoi le garde-fou est désactivé.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/EntryHandler.java:134adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/TransferBalanceSheet.java:20
Contre-vérification — Exemption vérifiée : EntryHandler.java:134-137 saute le contrôle NON_ZERO_BALANCE pour tous les sous-types TransferBalanceSheet (TenantToOwner.java:19, TenantToAgency.java:18, SupplierToOwner.java:23, GliToOwner.java:18), et aucun autre garde-fou d'équilibre n'existe sur la voie d'écriture. La spécification (adb-doc/.../events/tenant-to-owner.md) attend une comptabilisation équilibrée 800000/466100, donc l'exemption est injustifiée ; EntryHandlerTest.java ne l'exerce jamais. Mais elle est strictement latente : les deux constructeurs de règles en service (PostingRuleFactory.java:342 transfer(), :186 tenantToOwnerPayment()) émettent un débit + un crédit à partir du même montant, donc aucune entrée actuelle ne peut persister un déséquilibre. Sévérité abaissée à faible (filet de sécurité manquant, non un défaut actif).
Je ne considère pas cela comme critique au vu des flux actuels. Conserver la validation d'équilibre pour ces événements resterait toutefois un durcissement utile.
Les fichiers de prélèvement générés pour la banque omettent des mentions obligatoires du standard SEPA, notamment l'indication qu'il s'agit d'un premier prélèvement ou d'un prélèvement récurrent. Les banques françaises refusent couramment les fichiers sans ces mentions. De plus, le système ne sait pas distinguer un premier prélèvement d'un prélèvement récurrent, ni repartir sur un « premier » après un rejet, ni signaler une modification de mandat. Conséquence : des remises de prélèvements de loyers peuvent être rejetées en bloc, avec des encaissements en retard et un traitement manuel avec la banque.
Le pain.008 généré omet le bloc PmtTpInf obligatoire (SvcLvl SEPA / LclInstrm CORE / SeqTp) et ne gère aucun cycle de vie FRST/RCUR
Le mapper XML ignore explicitement le bloc PmtTpInf : les fichiers SDD exportés ne portent donc ni Service Level, ni Local Instrument, ni Sequence Type. Les guides d'implémentation EPC SDD Core rendent SvcLvl=SEPA, LclInstrm=CORE et SeqTp obligatoires ; les banques françaises rejettent couramment les fichiers pain.008 qui en sont dépourvus. Aucun suivi FRST/RCUR n'existe dans le domaine (le grep de FRST/RCUR/SeqTp ne remonte que des XSD et un fichier d'exemple hérité), si bien que même si l'élément était émis, aucun état ne permettrait de distinguer un premier prélèvement des prélèvements récurrents ni de réinitialiser la séquence après un rejet. AmdmntInd est en outre codé en dur à « false », rendant impossible la signalisation d'une modification de mandat.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/AbstractDirectDebitXmlMapper.java:80adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/AbstractDirectDebitXmlMapper.java:158adb-contracts/src/test/resources/xml/pain.008.xml:19
Contre-vérification — Redérivé : AbstractDirectDebitXmlMapper.java:80 ignore pmtTpInf et :158 code AmdmntInd=false en dur ; DirectDebitExportServiceImpl.asXml sérialise directement la sortie du mapper via JAXB sans enrichissement ni validation, et pain.008.001.02.xsd:251/593 marque PmtTpInf minOccurs=0, donc la sérialisation réussit silencieusement sans SvcLvl/LclInstrm/SeqTp — éléments rendus obligatoires par les guides EPC SDD Core C2B. Le grep de SeqTp/FRST/RCUR sur tout le dépôt ne remonte que l'exemple hérité (test/resources/xml/pain.008.xml, produit par l'ancien système « PRLO »). Une nuance : depuis la modification du rulebook SMNDA de 2016, une valeur constante SeqTp=RCUR suffit pour les premiers prélèvements, donc la sous-affirmation « cycle de vie FRST/RCUR manquant » est exagérée ; c'est l'absence même du bloc PmtTpInf qui constitue le vrai défaut non protégé. La sévérité haute est correcte (le rejet du fichier par la banque bloque le quittancement, mais sans corruption de données).
Je ne peux pas confirmer ce constat. Je n'ai pas implémenté cet export et j'ignore si tous ces champs étaient obligatoires pour la version de pain.008 et l'intégration bancaire du projet. L'implémentation s'appuyait, je crois, sur la documentation pertinente. À vérifier contre la spécification bancaire/EPC réelle et les fichiers précédemment acceptés avant de qualifier de bug.
Dès que le fichier de prélèvement est préparé, le loyer est automatiquement enregistré comme encaissé — avant même que la banque n'ait exécuté quoi que ce soit. Si la banque rejette ensuite le prélèvement (compte insuffisamment approvisionné, mandat révoqué), rien dans le système n'en tient compte : le compte du locataire affiche le loyer réglé, la facture reste soldée, aucune relance ne part, et la période ne peut plus être re-prélevée. Sans une vérification et une correction manuelles à chaque retour bancaire, l'agence croit avoir encaissé un argent qu'elle n'a pas reçu, et les comptes-rendus de gérance sont faux.
Aucune gestion des R-transactions (rejet de prélèvement) : le paiement est comptabilisé automatiquement dès la génération du fichier et PROCESSED est une impasse terminale
Lorsqu'un item passe à PROCESSED, la comptabilité crée et comptabilise immédiatement un paiement locataire entrant (isAutomaticBooking()=true) daté à la date de prélèvement — avant même que la banque n'ait rien exécuté. Si la banque retourne ensuite le prélèvement (provision insuffisante, mandat annulé), rien n'ingère ce retour : aucun parsing camt.054/pain.002 n'existe, DirectDebitStatus ne comporte que INITIAL/PROCESSED/ERROR (pas de REJECTED), et propose (filtre `!PROCESSED`) comme process (`List.of(INITIAL)`) excluent les items PROCESSED, de sorte que la période ne peut jamais être re-prélevée. Le grand livre du locataire montre le loyer payé, la facture reste lettrée et la relance ne se déclenche jamais, à moins que le personnel ne remarque le retour bancaire et ne contrepasse manuellement le paiement.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitStatus.java:4adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.java:196adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/creation/impl/DirectDebitPaymentCreationStrategy.java:53adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessDirectDebitRepositoryStrategy.java:63
Contre-vérification — Redérivé de bout en bout : DirectDebitStatus.java:4 ne comporte que INITIAL/PROCESSED/ERROR ; ProcessDirectDebitRepositoryStrategy.java:63 ne récupère que les INITIAL et propose filtre !PROCESSED (ProposeDirectDebitRepositoryStrategy.java:109,131 — seul ERROR est réinitialisé à INITIAL) ; ContractsFunctions.java:191-213 crée le paiement sur le message DIRECT_DEBIT_PROCESSED_QUEUE au moment du process, et DirectDebitPaymentCreationStrategy.java:52-54 + PaymentServiceImpl.java:117,446-456 le comptabilisent automatiquement daté à directDebitDate. Le grep sur tout le monorepo ne trouve que le pain.008 sortant (adb-contracts/pom.xml:280), aucune ingestion camt.054/pain.002/rejet, et aucun événement de rejet dans le catalogue documenté des 67 événements. DunningServiceImpl.java:220-227 base la relance sur le solde du locataire, que le paiement auto-comptabilisé lettre. Seul ReversedPaymentCreationStrategy manuel existe. La sévérité haute tient.
Comportement explicitement approuvé par Pierre. L'exigence était de créer l'événement de paiement à la confirmation du prélèvement par l'utilisateur et de le comptabiliser automatiquement. La comptabilisation à la confirmation est donc attendue ; la gestion automatique des rejets bancaires ne faisait pas partie du flux approuvé.
La validité du mandat est vérifiée uniquement au moment où le prélèvement est préparé, pas au moment où il est réellement lancé. Entre ces deux étapes — qui peuvent être séparées de plusieurs jours dans le déroulé habituel — un locataire peut révoquer son mandat sans que cela n'arrête rien : le prélèvement part quand même et l'encaissement est comptabilisé. C'est un prélèvement non autorisé, contestable à 100 % par le locataire, avec un risque juridique et de réputation pour l'agence. Par ailleurs, le système ne vérifie pas la date de prise d'effet du mandat : on peut prélever avant que le mandat ne soit effectif.
L'étape process prélève sur un instantané de validité du mandat figé au moment du propose — un mandat révoqué est toujours prélevé
Au moment du propose, l'item enregistre un instantané SepaMandateValidation (state==SIGNED, date de fin vs date de prélèvement). La stratégie de process recharge l'item persisté et ne valide que cet instantané stocké — elle ne re-récupère jamais le mandat. Si le locataire révoque le mandat (TERMINATE renseigne `end`) ou s'il est annulé entre le propose et le process (qui peuvent être espacés de plusieurs jours dans le flux UI en deux étapes), postHandle marque quand même l'item PROCESSED, le pain.008 inclut toujours le prélèvement et la comptabilité comptabilise le paiement — un prélèvement non autorisé. La validation ignore également la date `begin` du mandat (signedDate+7), si bien qu'un prélèvement peut être daté avant l'entrée en vigueur du mandat.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessDirectDebitRepositoryStrategy.java:131adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProposeDirectDebitRepositoryStrategy.java:285adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/SepaMandateValidation.java:29
Contre-vérification — Redérivé de façon indépendante. Le propose enregistre l'instantané (ProposeDirectDebitRepositoryStrategy.java:285-287) ; il est persisté (DirectDebitEntity.java:68). Le process recharge les items INITIAL et son initializeItem (ProcessDirectDebitRepositoryStrategy.java:125-129) ne re-récupère jamais le mandat ; isValidDirectDebitItem (131-136) ne vérifie que l'instantané figé avant de passer à PROCESSED. Aucun garde-fou ailleurs : TerminateAction ne fait que renseigner `end` sur l'entité mandat ; findDirectCreditCandidates (ContractRepository.java:277-291) ne filtre que sur l'état du contrat de location et le moyen de paiement ; l'export pain.008 (DirectDebitExportServiceImpl) ne re-contrôle pas le mandat ; aucun test ne couvre SepaMandateValidation. SignAction confirme begin=signedDate+7, jamais validé (SepaMandateValidation.java:29-34). L'art. L133-24 du CMF (13 mois, non autorisé) est correctement cité. La sévérité haute est juste.
Intentionnel. Un en-tête est créé une fois par un déclencheur, et propose rafraîchit le snapshot quand les données pertinentes changent — cela évite de répéter un calcul coûteux et donne à l'utilisateur une vue stable de ce qui sera traité. L'étape process confirme le snapshot existant plutôt que de le recalculer. Une revalidation légère et séparée du statut courant du mandat est faisable, mais ce serait une nouvelle décision technique/métier.
Deux failles de cloisonnement. D'une part, quand une agence consulte la liste de ses remises de prélèvements sur une période, elle voit aussi les références de remises de toutes les autres organisations sur la même période — une fuite de données entre clients de la plateforme. D'autre part, une instruction de paiement peut faire référence au mandat de prélèvement d'une autre organisation sans que le système ne s'y oppose, et déclencher ensuite des prélèvements sur cette base. Risque de confidentialité et de prélèvements imputés au mauvais mandat.
Failles de multi-tenancy : findDistinctTransactionIds ignore organisationId ; le existsById de PaymentInstructionValidator n'est pas cloisonné par organisation
DirectDebitRepositoryImpl.findDistinctTransactionIds reçoit l'organisationId mais construit la requête Mongo sans lui : toute agence listant ses transactions de prélèvement pour une période voit les identifiants de transaction (et horodatages de création) de toutes les autres organisations ayant des items PROCESSED sur cette période. Par ailleurs, PaymentInstructionValidator vérifie les mandats SEPA référencés par un simple `contractRepository.existsById(targetId)` — sans filtre d'organisation ni vérification que le mandat appartient au locataire du contrat de location — de sorte qu'une instruction de paiement peut légalement référencer l'id de mandat d'une autre organisation et piloter ensuite des prélèvements contre lui.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DirectDebitRepositoryImpl.java:254adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/PaymentInstructionValidator.java:52
Contre-vérification — Partie 1 confirmée : DirectDebitRepositoryImpl.java:254-264 ignore organisationId (toutes les requêtes voisines l'ajoutent), et GET /{period}/transactions (DirectDebitController.java:77-87) retourne les ids de transaction + dates de création de toutes les organisations. Le préjudice de la partie 2 est réfuté : existsById (PaymentInstructionValidator.java:52) n'est effectivement pas cloisonné, mais aucun prélèvement ne peut s'ensuivre — ProposeDirectDebitRepositoryStrategy.findSepa:308 résout les mandats via SepaMandateContractServiceImpl.getSepaMandateContract:161 cloisonné par organisation (findByIdForOrganization) ; un mandat étranger fait passer l'item à ERROR/invalid, et ProcessDirectDebitRepositoryStrategy:63,131-135 ne traite que les items INITIAL valides. Impact net : fuite inter-tenant d'ids/horodatages opaques uniquement → sévérité moyenne, pas haute.
Partiellement valide. findDistinctTransactionIds doit inclure organisationId dans sa requête Mongo — sans cela, des métadonnées de transaction peuvent être renvoyées entre organisations. En revanche, PaymentInstructionValidator.existsById() ne nécessite pas forcément de portée organisation, l'identifiant d'instruction de paiement étant globalement unique.
Quand plusieurs mandats se partagent un loyer, rien ne vérifie que les pourcentages de répartition totalisent bien 100 %. Si le total dépasse 100 %, le contrôle du plafond arrive trop tard : les prélèvements sont déjà validés et enregistrés, et le message d'alerte est même effacé au passage à l'étape suivante. Résultat : le ou les payeurs sont prélevés de plus que le montant dû, sans blocage ni avertissement visible. À l'inverse, une répartition inférieure à 100 % passe aussi sans alerte, et le loyer est alors sous-collecté.
Le garde-fou du montant maximal autorisé s'exécute après finalisation des items et les pourcentages de quote-part ne sont jamais validés — la sur-collecte est persistée en PROCESSED
Les montants des items valent `contractAmount x quote-part%`. Rien ne valide que les quotes-parts de la paymentInstruction totalisent 100 % (PaymentInstructionValidator ne vérifie que le type/l'existence de la cible). Si les quotes-parts dépassent 100 %, le total excède maxAllowedDirectDebitAmount, mais le contrôle a lieu dans handleContractDirectDebitAmount APRÈS que postHandle a déjà passé chaque item à PROCESSED (ou INITIAL) — il se contente d'ajouter un message de validation d'en-tête et persist() enregistre les items malgré tout. Pire, l'initializeHeader de la stratégie de process efface header.validationMessages avant postHandle, si bien qu'un en-tête signalé au moment du propose est silencieusement blanchi. Résultat : le locataire est prélevé au-delà de ce qu'il doit. Des quotes-parts totalisant moins de 100 % sous-prélèvent silencieusement. Les montants d'item nuls/négatifs (locataire créditeur) ne sont pas non plus filtrés avant l'export.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DirectDebitServiceImpl.java:183adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DirectDebitServiceImpl.java:265adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessDirectDebitRepositoryStrategy.java:121adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/PaymentInstructionValidator.java:33
Contre-vérification — Redérivé : postHandle passe à PROCESSED et transactionId est estampillé (DirectDebitServiceImpl.java:185-189) avant handleContractDirectDebitAmount (:190), qui n'ajoute qu'un message d'en-tête (:275) ; persist (:191) enregistre quand même. ProcessDirectDebitRepositoryStrategy.java:121 efface les drapeaux d'en-tête posés au propose, de sorte que isValidDirectDebitItem (:135) passe. PaymentInstructionValidator.java:33-61 ne vérifie jamais la somme des quotes-parts ; le chemin d'export (DirectDebitRepositoryImpl.java:285, AbstractDirectDebitXmlMapper:152) n'a aucun contrôle de montant/validation. Cependant, le seul client de production bloque le déclencheur : adb-ui accounting-payment-method-entries.component.ts:281-292 impose 0-100 par quote-part et somme<=100 ; les sommes inférieures à 100 sont autorisées par conception dans l'UI, donc le « sous-prélèvement » est intentionnel. Sévérité abaissée à moyenne : l'exploitation nécessite des écritures API directes, et le délai de remboursement SEPA de 8 semaines limite la perte.
Le système vérifie déjà le montant proposé dans handleContractDirectDebitAmount et handleProposedSepaDirectDebitAmount ; s'il dépasse la valeur autorisée, PROPOSED_AMOUNT_GREATER est ajouté au résultat de validation et affiché à l'utilisateur. La proposition invalide est intentionnellement persistée comme snapshot avec une erreur de validation. Le garde-fou de montant maximal n'est donc pas absent. La question de savoir si les quotes-parts doivent sommer exactement à 100 % reste une règle métier distincte.
Le calcul des montants à prélever (loyer x pourcentage de répartition, déduction d'allocations type CAF) produit des montants avec de nombreuses décimales qui sont enregistrés et transmis tels quels à la banque. Or le standard SEPA n'accepte que deux décimales : la banque peut rejeter tout le fichier. Et même quand elle l'accepte, la comptabilité applique de son côté un arrondi à deux décimales, si bien que le montant prélevé et le montant comptabilisé peuvent différer d'un centime — des écarts de rapprochement bancaire à traiter à la main.
Montants calculés en arithmétique double et stockés sans arrondi — montants pain.008 invalides et dérive de centimes vs comptabilité
DirectDebitItem.sepaDebitAmount convertit un BigDecimal en double, multiplie (`fullAmount * rate`, soustraction de l'abattement) et réencapsule le double brut via Quantity.euro(Double) -> BigDecimal.valueOf(double) sans arrondi à 2 décimales. Exemple : montant du contrat 1000,10, quote-part 33,33 % -> 333,3333300000000... des artefacts comme 333,36330000000004 sont persistés et mappés tels quels dans les champs pain.008 `InstdAmt`/`CtrlSum` (source = sepaDirectDebitAmount.amount), violant la règle SEPA du maximum de 2 décimales -> rejet du fichier par la banque. Parallèlement, la comptabilité convertit la même Quantity via Money, qui arrondit à 2 décimales HALF_DOWN, si bien que même lorsque la banque tolère/tronque, le paiement comptabilisé peut différer d'un centime du montant prélevé. handleProposedSepaDirectDebitAmount reproduit le schéma (`sum + proposed.doubleValue()`, `newAmount > max` sur des doubles).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitItem.java:108adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DirectDebitServiceImpl.java:292adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/Quantity.java:77adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/Money.java:97
Contre-vérification — Redérivé : DirectDebitItem.java:108-127 fait des calculs en double (rate, fullAmount*rate, soustraction de l'abattement) et encapsule via Quantity.euro(Double) -> BigDecimal.valueOf (sans setScale). Ex. 1000,01 à 33,33 % -> 333,303333 ; 1000,02 à 40 % -> 400,00800000000004. Aucun arrondi nulle part sur le chemin : ProposeDirectDebitRepositoryStrategy:278 le pose brut, QuantityEntity le persiste brut, AbstractDirectDebitXmlMapper.java:85/152/191 mappe le montant tel quel dans InstdAmt/CtrlSum, et DirectDebitExportServiceImpl sérialise sans validation de schéma — violant le fractionDigits=5 du XSD pain.008.001.02 et la règle SEPA des 2 décimales. Money.java:96-98 arrondit HALF_DOWN à 2 décimales, confirmant la moitié « dérive de centimes ». Le test unitaire DirectDebitItemTest fait ses assertions avec DELTA=0.1, ce qui masque le défaut. Les quotes-parts fractionnaires sont une fonctionnalité voulue (le test utilise 1-80 %). La sévérité moyenne est juste : échec bruyant (la banque rejette le fichier), ne se déclenche que sur des quotes-parts non-100 %/des abattements.
Problème technique valide. Les calculs monétaires ne doivent pas passer par doubleValue() : la conversion en flottant binaire peut créer des artefacts d'arrondi et décaler le montant SEPA d'un centime. Le calcul doit utiliser BigDecimal ou l'API Money/Quantity existante de bout en bout, et les tests de nullité doivent utiliser compareTo(BigDecimal.ZERO) == 0 plutôt que BigDecimal.equals(), sensible à l'échelle. Le montant final exporté doit être calculé en arithmétique décimale ou, au minimum, explicitement arrondi et formaté à deux décimales avant la génération du pain.008.
L'export du fichier bancaire ne garde aucune trace : le système ne sait pas qu'une remise a déjà été envoyée. Relancer l'export (par erreur, après un problème de téléchargement, ou avec une période qui chevauche) produit un second fichier qui, s'il est remis à la banque, prélève les mêmes locataires une deuxième fois. Par ailleurs, l'export accepte n'importe quel filtre : on peut inclure dans un vrai fichier bancaire des prélèvements non encore validés, y compris avec des mandats non contrôlés. Risque direct de doubles prélèvements et de prélèvements indus, avec remboursements et contestations à gérer.
L'export XML accepte n'importe quelle requête et n'a aucun garde-fou exporté/collecté — les mêmes prélèvements peuvent être envoyés deux fois à la banque, et des items INITIAL/ERROR peuvent être exportés
GET /contracts/direct-debit/export sérialise tout ce que la DirectDebitQuery fournie par l'utilisateur fait correspondre. Rien n'impose statuses=[PROCESSED] : une requête vide exporte des items INITIAL et ERROR (mandats non validés, montants non traités) dans un fichier pain.008 réel — le propre test du module fait exactement `export(new DirectDebitQuery())`. Il n'existe aucun état « EXPORTED », aucun journal d'export, et MsgId est salé avec currentTimeMillis tandis que PmtInfId/EndToEndId restent constants par item, si bien que réexécuter l'export (requête de période chevauchante, retry après erreur de téléchargement) produit un second fichier syntaxiquement distinct qui reprélève les mêmes locataires s'il est soumis.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/DirectDebitController.java:53adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DirectDebitServiceImpl.java:131adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DirectDebitRepositoryImpl.java:292
Contre-vérification — Vérifié : DirectDebitController.java:53 accepte n'importe quelle DirectDebitQuery (aucune contrainte) ; CriteriaHelper.in:38 saute le filtre de statut lorsqu'il est vide, si bien qu'une requête vide/limitée à la période exporte des items INITIAL/ERROR/PROCESSED sur plusieurs lots ; DirectDebitStatus n'a pas d'état EXPORTED et aucun journal d'export n'existe ; MsgId est salé avec currentTimeMillis (AbstractDirectDebitXmlMapper.java:229) tandis que PmtInfId/EndToEndId restent constants, donc un réexport déjoue la détection de fichier en double de la banque. Les mitigations le maintiennent à moyen : le seul appelant UI (adb-ui contracts.service.ts:1679) cible toujours par transactionId (PROCESSED uniquement), le retraitement est bloqué (ProcessDirectDebitRepositoryStrategy ne consomme que les INITIAL), et les items INITIAL/ERROR n'ont pas d'easyId, si bien que de tels fichiers échoueront probablement à la validation XSD de la banque.
Vraisemblablement une lacune d'intégration valide. L'export n'a peut-être pas été pleinement aligné sur la dernière implémentation de prélèvement. Il devrait restreindre les exports aux items éligibles déjà traités et empêcher qu'un même prélèvement soit envoyé plus d'une fois — par exemple via un statut d'export, un identifiant de lot ou un enregistrement d'audit.
La réglementation SEPA impose d'informer le débiteur avant chaque prélèvement (pré-notification). Le système n'a aucune fonction de ce type : le locataire n'est jamais prévenu à l'avance, ce qui expose l'agence à des contestations. Par ailleurs, la date de prélèvement est figée dès la préparation de la remise ; si l'agence traite ou exporte le fichier après cette date — cas fréquent en cas de retard — le fichier porte une date déjà passée et la banque rejette la remise entière, retardant tous les encaissements du mois.
Aucune pré-notification du débiteur et aucun contrôle de délai — ReqdColltnDt peut même être dans le passé
Aucune notion de pré-notification n'existe nulle part (le grep de prenotif/pre-notification sur contracts, accounting, model et UI ne renvoie rien) : le débiteur n'est jamais notifié N jours avant le prélèvement et aucun contrôle ne lie directDebitDate à une date de notification. De plus, directDebitDate est calculée purement à partir de period+debitDay au moment du propose ; si l'agence traite/exporte après ce jour (fréquent pour les traitements tardifs), le champ pain.008 `ReqdColltnDt` est une date passée et toute la remise est rejetée par la banque — il n'existe aucun plancher tel que max(aujourd'hui + délai de règlement, debitDay).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProposeDirectDebitRepositoryStrategy.java:334adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/AbstractDirectDebitXmlMapper.java:86
Contre-vérification — Redérivé depuis le code. ProposeDirectDebitRepositoryStrategy.java:334-346 borne directDebitDate uniquement à [jour 1, fin de mois] de la période — aucun plancher vs aujourd'hui+délai ; initializeHeader (ligne 242) la stocke telle quelle. AbstractDirectDebitXmlMapper.java:86 et toXmlDate (lignes 202-209) la recopient dans ReqdColltnDt avec seulement une conversion de fuseau horaire ; DirectDebitServiceImpl.export (ligne 131) n'ajoute aucun garde-fou de date, et les validationMessages d'en-tête ne couvrent que les contrôles de montant (lignes 258/275). Le grep de prenotif/prénotif ne renvoie rien, et le template call-for-rent.html ne mentionne jamais SEPA/date de prélèvement, donc aucun document ne peut servir de pré-notification EPC. Exagération mineure : un ReqdColltnDt passé peut être auto-décalé plutôt que rejeté, selon la banque. La sévérité moyenne est correcte.
Je ne peux pas confirmer ce constat car je ne me souviens pas que ce flux ait été discuté. Il faudrait clarifier si la pré-notification était gérée manuellement, par un autre service, ou hors périmètre du projet. Cela requiert une vérification métier et de spécification plutôt qu'une qualification immédiate en bug.
Trois défauts de moindre gravité. Un mandat inutilisé depuis plus de 36 mois est caduc selon les règles SEPA, mais le système ne suit pas la date du dernier prélèvement et continue de prélever. Relancer le traitement d'une remise efface définitivement les prélèvements précédemment mis en anomalie ainsi que leurs messages d'erreur : les problèmes détectés disparaissent sans laisser de trace. Enfin, quand un loyer est réparti entre plusieurs mandats, le total affiché de la remise peut être remplacé par le montant d'une seule quote-part, donc sous-estimé dans ce qui est enregistré et présenté.
Aucun suivi de caducité de mandat à 36 mois ; le process efface les items ERROR historiques ; forSepa écrase le total de l'en-tête avec une seule quote-part
Trois lacunes d'intégrité mineures. (1) SepaMandateValidation ne vérifie que l'état SIGNED et la date de fin — rien ne suit le dernier prélèvement par mandat, si bien qu'un mandat inutilisé depuis 36 mois ou plus (caduc selon le rulebook EPC) est toujours prélevé. (2) saveItems supprime TOUS les items INITIAL et ERROR d'un en-tête avant de réinsérer le lot courant : lancer process (qui ne charge que les items INITIAL) efface définitivement les items ERROR précédemment persistés et leurs messages de validation. (3) Dans le flux single-sepa, handleContractDirectDebitAmount recalcule header.contractDirectDebitAmount à partir de List.of(item), écrasant le total multi-mandats correct par le montant d'une seule quote-part ; l'en-tête sous-évalué est persisté et affiché dans les rapports.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/SepaMandateValidation.java:29adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DirectDebitRepositoryImpl.java:166adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DirectDebitServiceImpl.java:223
Contre-vérification — Les trois redérivés. (1) SepaMandateValidation.java:29-33 ne vérifie que l'état SIGNED et la date de fin ; SepaMandateContract.java et AbstractContract.java n'ont aucun champ de dernier prélèvement, et aucune logique de 36 mois/caducité n'existe nulle part dans le dépôt — la règle de caducité à 36 mois du rulebook EPC SDD est correctement énoncée. (2) DirectDebitRepositoryImpl.java:163-175 supprime tous les items INITIAL+ERROR de l'en-tête avant d'insérer le lot, tandis que ProcessDirectDebitRepositoryStrategy.java:63 ne charge que les INITIAL ; les items ERROR précédemment persistés via le chemin single-sepa (saveItem, sans suppression) sont effacés avec leur validationMessage au prochain process complet. (3) DirectDebitServiceImpl.java:224-228 passe List.of(item) à handleContractDirectDebitAmount (:265-277), qui écrase header.contractDirectDebitAmount — y compris la somme multi-mandats correcte posée en :306 — par une seule quote-part ; persist() (:341) enregistre l'en-tête sous-évalué. La sévérité basse est juste : les montants d'item réellement prélevés et les écritures ne sont pas affectés.
Trois points distincts. La règle d'inactivité de mandat à 36 mois ne m'est pas familière et nécessite une revue métier/spécification. Le remplacement ou la suppression des items en ERROR pendant le traitement est attendu (les items traités ne doivent pas rester en erreur). Un total d'en-tête représentant une quote-part individuelle peut être intentionnel, l'utilisateur pouvant payer en plusieurs fois ; la sémantique exacte des champs XML devrait être vérifiée avant d'y voir une erreur.
Rien n'empêche de relancer le traitement d'un bordereau CAF déjà payé, et le système est incapable de reconnaître qu'il a déjà enregistré ces encaissements. Chaque relance crédite donc une seconde fois l'aide de chaque locataire sur son compte : sur un bordereau de 50 locataires à 300 EUR, cela fait 15 000 EUR d'aides CAF comptés deux fois. Le cas se produit facilement : si le traitement échoue en cours de route (par exemple sur une ligne à montant nul), une partie des encaissements est déjà passée et une relance duplique ces lignes. Les soldes locataires et les comptes rendus de gestion s'en trouvent faussés.
Le processus de bordereau n'est pas idempotent : aucun garde-fou d'état et la clé d'unicité des paiements MANUAL utilise l'epoch en millisecondes, si bien qu'un retraitement comptabilise deux fois l'argent de la CAF
POST /accounting/allowance-statements/{id}/process peut être appelé un nombre illimité de fois (ni le contrôleur ni le service ne vérifient que le bordereau est déjà PAID). Chaque ligne locataire devient un Payment de paymentOrigin MANUAL, dont la clé de déduplication est origine+sens+epoch courant en millisecondes, de sorte que UniquenessHandler ne peut jamais détecter un rejeu. Scénario : l'utilisateur (ou un appel HTTP rejoué après une erreur en cours de flux, p. ex. une ZeroAmountException sur une ligne interrompt le flatMap alors que certains paiements ont déjà été comptabilisés) relance process sur un bordereau de 50 locataires x 300 EUR -> 15 000 EUR de recettes CAF crédités deux fois aux comptes locataires 411100/RC, et les résultats des paiements déjà comptabilisés sont perdus car le bulkUpdate ne s'exécute jamais en cas d'erreur.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/RentalAllowanceStatementServiceImpl.java:169adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Payment.java:283adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/UniquenessHandler.java:13
Contre-vérification — Redérivé du code. RentalAllowanceStatementServiceImpl.java:169-172 : process() appelle doProcess puis patchState(PAID) sans aucune précondition d'état ; le contrôleur (RentalAllowanceStatementController.java:114-126) et la mise à jour du repository (RentalAllowanceStatementTemplateRepositoryImpl.java:103-114, simple findAndModify) n'en ajoutent aucune. Chaque ligne devient un Payment d'origine MANUAL (RentalAllowanceStatementMapper.java:74) ; ManualPaymentCreationStrategy.java:20 fixe unique=getKey(), et Payment.java:283-287 le construit à partir de origine+sens+epoch-millis, de sorte que ni l'index unique Mongo (PaymentServiceImpl.saveIfNotExists) ni UniquenessHandler ne peuvent jamais reconnaître un rejeu. L'affirmation d'échec partiel tient aussi : bulkUpdate ne s'exécute qu'après collectList (RentalAllowanceStatementServiceImpl.java:220-221), donc une erreur en cours de flux perd les résultats comptabilisés et laisse l'état non-PAID, invitant à un nouvel essai. La sévérité élevée est correcte (nécessite un appel dupliqué ; les paiements sont réversibles).
Problème de sévérité haute, valide. Le même bordereau d'allocation peut aujourd'hui être traité plusieurs fois car rien n'empêche le retraitement après passage à PAID ; comme les paiements générés utilisent la clé MANUAL habituelle, la clé horodatée ne protège pas ce flux d'une double comptabilisation. L'endpoint de traitement doit rejeter un bordereau déjà traité, et idéalement la transition d'état doit être atomique pour que deux requêtes concurrentes ne puissent pas traiter deux fois le même bordereau.
Quand aucun bordereau CAF payé n'existe encore pour la période — le cas normal, la CAF payant à terme échu — le calcul du prélèvement ne prend en compte l'aide que d'un seul locataire du bail, choisi arbitrairement. Dans une colocation où plusieurs colocataires ont chacun leur aide CAF versée à l'agence, tous les autres sont prélevés du loyer plein, sans déduction de leur APL. Résultat : des locataires prélevés en trop, des comptes créditeurs à régulariser et des remboursements à gérer manuellement.
Le mécanisme de repli des allocations en prélèvement automatique n'utilise que le premier contrat d'allocation, ignorant les autres locataires d'un même bail (colocation)
Lorsqu'aucun bordereau PAID n'existe pour la période de prélèvement (le cas normal, puisque la CAF paie à terme échu), socialAllowances() se replie sur `racList.get(0)` et construit une seule SocialAllowance pour ce locataire unique. Pour un bail en colocation où plusieurs colocataires ont chacun un RentalAllowanceContract SIGNED et leur propre quote-part SEPA, tout locataire autre que racList[0] ne bénéficie d'AUCUNE déduction d'allocation : sepaDebitAmount ne soustrait rien, et le locataire est débité de la totalité de sa quote-part de loyer alors même que la CAF en verse une partie directement à l'agence -> locataire surdébité, d'où solde créditeur et remboursements à répétition par la suite.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProposeDirectDebitRepositoryStrategy.java:357adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProposeDirectDebitRepositoryStrategy.java:382
Contre-vérification — Redérivé. Plusieurs contrats d'allocation SIGNED par bail sont un cas de première classe : adb-accounting en crée un par tenantPayment (RentalAllowanceStatementMapper.java:89-107, « 1-à-1 entre locataire et contrat »), enregistrés SIGNED (RentalAllowanceContractServiceImpl.java:79-83). La requête non triée (ContractRepository.java:332) les renvoie tous, pourtant le repli construit une seule SocialAllowance à partir d'un racList.get(0) arbitraire (ProposeDirectDebitRepositoryStrategy.java:357,382-387), tandis que le chemin via bordereau PAID regroupe correctement par locataire (:362-368). Le rapprochement par ligne selon le récepteur du contrat bancaire (:266-273) laisse les autres colocataires null, et sepaDebitAmount ne déduit rien quand l'allocation est null (DirectDebitItem.java:117-125) — la quote-part complète est débitée. Aucun test ne couvre ce cas ; la formule documentée est agnostique par locataire. La sévérité élevée est correcte : surdébit, mais limité au cas colocation+repli, et les propositions sont revues par l'utilisateur.
Techniquement suspect mais nécessite une clarification métier. racList.get(0) signifie qu'un seul contrat d'allocation locataire est pris en compte pendant le repli ; pour un bail en colocation, cela peut ignorer les allocations des autres locataires. Que le bon comportement soit de sommer toutes les allocations dépend des règles métier voulues pour le prélèvement, je ne classerais donc pas ceci comme un bug confirmé sans confirmation de la spécification.
Dès la création d'un bordereau CAF, même en simple brouillon, le système écrase la fiche d'aide de chaque locataire : montant de l'aide, montant attendu pour les prochains prélèvements, date de début, et il remet en vigueur des aides pourtant suspendues ou résiliées. Supprimer le brouillon ne rétablit rien : les anciennes valeurs sont perdues. Concrètement, un gestionnaire qui saisit un montant erroné puis supprime son brouillon laisse derrière lui un montant faux qui pilote silencieusement tous les prélèvements SEPA suivants.
Le contrat d'allocation, clé sur l'ID du locataire, est upserté de façon destructive à chaque création de bordereau (même en DRAFT), sans annulation lors de la suppression du bordereau
RentalAllowanceStatementServiceImpl.create() upserte immédiatement un RentalAllowanceContract par locataire (contract._id = id de la personne locataire), écrasant allowanceAmount/nextExpectedAllowanceAmount, la date de début, fromSupplier, forçant state=SIGNED (ressuscitant les contrats SUSPENDED/TERMINATED), et remplaçant relatedContracts par l'unique contrat de location de cette ligne de bordereau. Cela survient alors que le bordereau est encore en DRAFT et n'est jamais annulé si le bordereau est supprimé (deleteById ne supprime que le bordereau et ses lignes de paiement). Scénario : (a) l'utilisateur ébauche un bordereau avec un montant erroné puis le supprime -> le mauvais nextExpectedAllowanceAmount pilote silencieusement toutes les déductions SEPA ultérieures ; (b) un locataire avec deux baux avec APL -> chaque bordereau bascule relatedContracts vers un seul bail, de sorte que allowanceContractsForRentalContract() sur l'autre bail ne renvoie rien et son prélèvement automatique ignore totalement l'allocation.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/mapper/RentalAllowanceStatementMapper.java:95adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/RentalAllowanceContractServiceImpl.java:83adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/ContractTemplateRepositoryImpl.java:405adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/RentalAllowanceStatementServiceImpl.java:99
Contre-vérification — Redérivé du code : RentalAllowanceStatementServiceImpl.create():99-101 appelle createAllowanceContracts sur le DRAFT avant l'enregistrement ; RentalAllowanceContractServiceImpl.saveMany():79-83 clé le contrat sur l'id du locataire et force state=SIGNED en contournant sa machine à états ; ContractTemplateRepositoryImpl.saveMany():405-443 upserte sur _id+orgId avec un $set complet, écrasant montants/date de début/fromSupplier/relatedContracts et ressuscitant les contrats SUSPENDED/TERMINATED. deleteById():152-161 n'annule jamais rien. ProposeDirectDebitRepositoryStrategy.socialAllowances():352+ se replie sur rac.getNextExpectedAllowanceAmount() pour le SEPA et interroge relatedContracts.targetId, si bien que les deux scénarios d'échec (montant erroné-puis-suppression ; bascule à deux baux) se reproduisent. Aucun garde-fou, test ni documentation ne contredit.
Comportement intentionnel. Le bordereau en brouillon est déjà considéré comme la source de mise à jour du RentalAllowanceContract associé, y compris les montants d'allocation et l'état du contrat. L'absence de rollback lorsque le bordereau est ensuite supprimé n'est donc pas considérée comme un bug.
Quand la CAF récupère un trop-perçu d'APL (l'indu, situation courante après un changement rétroactif de droits), le bordereau contient des lignes négatives ou une compensation sur le mois suivant. Le système ne sait exprimer que des encaissements positifs : une ligne négative ou nulle fait échouer le traitement, ou doit être supprimée à la main. Il n'existe aucun mécanisme de reversement à la CAF ni de compensation. Ces reprises doivent donc être traitées hors système, avec un risque d'écarts entre la trésorerie réelle, les comptes locataires et les comptes rendus aux propriétaires.
L'indu / récupération CAF ne peut pas être enregistré : le sens du paiement est codé en dur à INCOMING et les montants négatifs ou nuls sont rejetés par le moteur d'écritures
Lorsque la CAF récupère un trop-perçu d'APL auprès de l'agence (indu — courant après des changements de droits rétroactifs), le bordereau contient des lignes négatives ou la CAF déduit la récupération du paiement du mois suivant. Le flux d'allocation ne peut pas l'exprimer : toPayment() code en dur le sens INCOMING pour chaque ligne, et PostingRuleHandler lève NegativeAmountException / ZeroAmountException pour tout montant d'écriture négatif ou nul. Le booléen `adjustment` du bordereau existe mais n'est qu'un filtre de recherche — aucune logique de contre-passation, de compensation ni d'écriture sortante vers la CAF n'existe où que ce soit. Scénario : un bordereau CAF pour 2026-06 affiche le locataire X à -180 EUR (récupération d'indu) ; le traitement du bordereau échoue (ou la ligne doit être silencieusement ignorée), le rapprochement bancaire de l'agence et le compte locataire 411100/RC divergent de la réalité, et des écritures manuelles hors circuit sont nécessaires.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/mapper/RentalAllowanceStatementMapper.java:71adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/PostingRuleHandler.java:84adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/RentalAllowanceStatementTemplateRepositoryImpl.java:133
Contre-vérification — Redérivé : RentalAllowanceStatementMapper.java:71 code en dur INCOMING ; toPayment amount = currentAmount (ligne 81, aucun contrôle de positivité dans TenantRentalAllowance ni dans la saisie UI, save-caf-payment.component.html:222). process() → PaymentEvent avec postingRuleGenerator=TRUE (PaymentEvent.java:57) → PostingRuleHandler.java:79/85 lève Zero/NegativeAmountException, avortant tout le Flux du bordereau (RentalAllowanceStatementServiceImpl.java:206-226), de sorte qu'un bordereau comportant une ligne à -180 ne peut pas être traité. Toutefois la sévérité est surestimée : la récupération EST enregistrable dans l'application via une contre-passation complète du paiement (PaymentController.java:102-116, REVERSE_ACCOUNTING_EVENT) ou via des écritures sortantes/manuelles (PostingRuleFactory.tenantPaymentIssued) — seuls le flux de bordereau d'allocation et les indus partiels/compensés ne sont pas pris en charge.
Constat de fonctionnalité manquante, valide — mais le problème n'est pas que le moteur d'écritures refuse les montants négatifs. Les montants de paiement doivent rester positifs : une reprise CAF (indu) doit plutôt être représentée comme un paiement SORTANT utilisant la valeur absolue de la ligne négative, et les lignes à zéro ne doivent pas créer de paiement. Ce qui manque, c'est un flux dédié de reprise CAF sortante, qui n'a pas été implémenté.
Lors du traitement d'un bordereau CAF, le système marque systématiquement le bordereau comme payé, sans vérifier si chaque ligne locataire a réellement été encaissée. Si une ligne sur quarante échoue, le bordereau affiche quand même payé. Conséquence en cascade : le calcul du prélèvement suivant fait confiance à ce statut et déduit l'aide du locataire concerné de son prélèvement SEPA, alors qu'aucun argent CAF n'a été encaissé pour lui. On sous-encaisse ce locataire, et personne n'est alerté.
process() marque le bordereau PAID même lorsque des paiements individuels de locataires se sont terminés en état ERROR
doProcess() stocke l'état de chaque PaymentData dans processingResult mais ne l'inspecte jamais ; process() bascule ensuite inconditionnellement le bordereau à PAID. La chaîne de handlers signale les échecs comme un rejet d'unicité en positionnant l'état de l'événement/paiement à ERROR sans lever d'exception (UniquenessHandler positionne l'état ERROR et poursuit). Scénario : 1 ligne sur 40 échoue -> le bordereau apparaît PAID ; en aval, ProposeDirectDebitRepositoryStrategy fait confiance aux bordereaux PAID (la requête filtre state=PAID) et déduit l'allocation du locataire en échec de son prélèvement SEPA alors qu'aucune trésorerie CAF n'a été comptabilisée -> sous-encaissement que personne ne signale.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/RentalAllowanceStatementServiceImpl.java:170adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/RentalAllowanceStatementServiceImpl.java:211
Contre-vérification — Redérivé : RentalAllowanceStatementServiceImpl.java:170-171 bascule à PAID sans condition ; 211-214 stocke paymentState sans le vérifier. ERROR est une valeur, non une exception : UniquenessHandler.java:19 (ainsi que les handlers PostingRule/LedgerAccount/Journal/FiscalYear/Period via Handler.error()) positionne ERROR ; DefaultHandlerChain.java:53-61 court-circuite et renvoie l'événement ; AbstractPaymentStrategy.java:39-40 le mappe en paiement ERROR ; BookPaymentAction/sink l'émet comme une valeur normale. Aucun garde-fou de contrôleur, aucun test pour le cas ERROR. En aval ProposeDirectDebitRepositoryStrategy.java:401 filtre state=PAID et :360-368 somme tous les tenantPayments, déduisant l'allocation en échec (DirectDebitItem.sepaDebitAmount:117-125). Sévérité moyenne : sous-encaissement silencieux, mais rattrapable via le solde du grand livre.
Problème valide. process() enregistre les résultats des paiements mais n'utilise pas leur état pour déterminer le statut final du bordereau. Le bordereau ne doit passer à PAID que lorsque tous les paiements locataires ont été traités avec succès ; si un paiement finit en ERROR, il doit rester rejouable ou passer à un état ERROR / PARTIALLY_PAID, selon le modèle d'état disponible.
Quand le système déduit l'aide CAF du prélèvement d'un bail, il additionne toutes les lignes d'aide du locataire présentes sur le bordereau de la période, sans distinguer à quel bail chaque ligne se rapporte. Si un locataire a deux baux avec chacun une aide (par exemple 200 EUR et 150 EUR) sur le même bordereau, le prélèvement du premier bail se voit déduire 350 EUR au lieu de 200 EUR. Ce bail est sous-encaissé de 150 EUR, pendant que l'autre est prélevé plein tarif — d'où des écarts de compte à régulariser à la main.
La déduction d'allocation somme les lignes de TOUS les contrats de location d'un bordereau rapproché, et non du seul contrat prélevé
La recherche de bordereaux filtre ceux qui CONTIENNENT le contrat de location (`tenantPayments.rentalContract.targetId`), mais le $lookup rejoint chaque ligne tenant_rental_allowance du bordereau. socialAllowances() flatMappe alors tous les tenantPayments et regroupe par id de locataire uniquement, perdant la dimension du contrat de location. Scénario : le locataire T détient deux baux A et B, tous deux avec des lignes d'allocation (200 EUR et 150 EUR) dans le même bordereau CAF PAID pour la période ; le prélèvement automatique du bail A calcule la socialAllowance de T à 350 EUR et la soustrait du débit du bail A -> le bail A est sous-encaissé de 150 EUR tandis que le bail B est débité en totalité.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProposeDirectDebitRepositoryStrategy.java:361adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/RentalAllowanceStatementTemplateRepositoryImpl.java:138
Contre-vérification — Redérivé. Le filtre du repository ne sélectionne que les bordereaux contenant le contrat (RentalAllowanceStatementTemplateRepositoryImpl.java:131), tandis que le $lookup (:138-144) joint chaque ligne tenant_rental_allowance par statementId ; le service (RentalAllowanceStatementServiceImpl.find) ne réalise aucun élagage par contrat. socialAllowances() (ProposeDirectDebitRepositoryStrategy.java:360-368) flatMappe tous les tenantPayments et regroupe par id de locataire uniquement, alors que payment.getRentalContract() existe (TenantRentalAllowance.java:32). sepaDebitAmount (DirectDebitItem.java:117-124) soustrait la somme gonflée. Une nuance : les deux baux correspondent au même bordereau, donc les DEUX débits soustraient les 350 EUR complets (subvention comptée deux fois), légèrement pire que rapporté. La spécification documentée et le repli sur RAC (:382-387) confirment l'intention par contrat. La sévérité moyenne tient.
Problème valide. Un bordereau peut contenir des paiements pour plusieurs baux ; après avoir trouvé un bordereau contenant le bail courant, l'agrégation rejoint actuellement toutes les lignes de paiement locataire de ce bordereau. Les lignes doivent être filtrées par le rentalContract.targetId courant — sinon une allocation appartenant à un autre bail peut réduire à tort le montant du prélèvement du bail courant.
Entre l'appel de loyer et le versement de la CAF (qui paie à terme échu), le compte du locataire affiche la totalité du loyer comme dette personnelle, part APL comprise. La relance automatique ne connaît pas l'existence des aides et peut donc réclamer au locataire un argent que la CAF doit à l'agence. Par ailleurs, le signalement d'impayé à la CAF, obligatoire au bout de deux mois consécutifs de loyer impayé, n'a aucun support dans l'outil : ni déclenchement, ni suivi. Cela expose le bailleur à des sanctions et dégrade la relation locataire.
Aucune créance CAF et aucune prise en compte de l'allocation dans le recouvrement : le locataire est affiché/relancé comme débiteur pour la part couverte par la CAF, et le signalement d'impayé obligatoire à la CAF est absent
L'allocation fonctionne purement en trésorerie : l'appel de loyer complet débite 411100/RC et rien ne représente la part CAF tant qu'un bordereau n'est pas traité (grep confirme l'absence de 'CAF'/'allowance' dans DunningServiceImpl ou le modèle de recouvrement, et l'absence de tout code d'attestation/quittance/signalement dans tout le dépôt). Conséquences pour un portefeuille en tiers payant : (1) entre l'appel de loyer et le bordereau CAF (payé à terme échu), le solde du locataire surestime sa dette personnelle du montant de l'APL et le recouvrement automatisé peut le cibler pour des sommes que la CAF lui doit ; (2) le signalement légalement obligatoire de la défaillance du locataire à la CAF/CDAPL (2 mois consécutifs impayés, CCH art. R.824-2) n'a aucun support — pas de déclencheur, pas de suivi — exposant le bailleur à une obligation de remboursement de l'APL ; la suspension du RentalAllowanceContract est une transition d'état purement manuelle.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:1adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/mapper/RentalAllowanceStatementMapper.java:69
Contre-vérification — Redérivé : l'appel de loyer débite 411100 en totalité (events/call-for-rent.md:43) ; le seul effet comptable de l'allocation est un paiement locataire ordinaire à la réception du bordereau (RentalAllowanceStatementMapper.java:63-83, RentalAllowanceStatementServiceImpl.java:169) ; aucun AccountingEventType ALLOWANCE/CAF n'existe. Le recouvrement est créé à partir du solde auxiliaire brut du locataire sans compensation d'allocation (DunningServiceImpl.java:220-247) ; aucun code CAF/signalement dans tout le dépôt ; SUSPEND est manuel (RentalAllowanceContractServiceImpl.java:176-215). Atténuations que le constat omet : DirectDebitItem.java:117-123 soustrait socialAllowance du SEPA, et les relances sont déclenchées par l'utilisateur. La règle légale (CCH R.824-1/R.824-2) est correctement énoncée. La sévérité moyenne tient.
Fonctionnalité métier manquante plutôt qu'un défaut local. Le modèle actuel reste en base caisse : la totalité du loyer est d'abord présentée comme dette du locataire, aucune créance CAF distincte n'est créée, et la relance ne distingue pas la part attendue de la CAF. Le signalement d'impayé à la CAF n'a pas non plus été implémenté. À traiter comme une fonctionnalité hors première version plutôt que comme un bug isolé.
Le montant du prélèvement (quote-part de loyer moins aide CAF) est calculé avec une arithmétique moins précise que celle utilisée pour la comptabilité. Dans certains cas (quotes-parts en pourcentage, montants non ronds), le montant prélevé peut différer d'un centime du montant inscrit au compte du locataire. Ces écarts d'un centime créent des soldes ouverts perpétuels qui empêchent le lettrage des comptes et génèrent du travail de pointage manuel.
sepaDebitAmount calcule des montants avec de l'arithmétique double et un test de zéro BigDecimal.equals sensible à l'échelle
Le montant SEPA (quote-part de loyer moins allocation) est calculé par multiplication et soustraction via doubleValue(), puis ré-encapsulé avec Quantity.euro(double) ; des artefacts de virgule flottante binaire peuvent décaler le montant débité d'un centime par rapport aux montants du grand livre calculés en BigDecimal (p. ex. quote-part 33,33 % de 1000,10 EUR moins allocation 210,07). De plus `factor.getAmount().equals(BigDecimal.valueOf(0))` est sensible à l'échelle (0.00 != 0), sauvé uniquement parce qu'un facteur 0.00 se multiplie de toute façon en zéro ensuite. Des écarts au centime entre le fichier SEPA et le compte locataire créent des soldes ouverts perpétuels de 0,01 qui bloquent le lettrage.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitItem.java:101adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitItem.java:114
Contre-vérification — Code confirmé : DirectDebitItem.java:101 equals sensible à l'échelle (inoffensif, comme le reconnaît le constat) et :108-127 arithmétique monétaire en double. Aucun arrondi n'existe en aval (Quantity.euro(Double)→BigDecimal.valueOf(double) ; QuantityMapper/QuantityEntity en passe-plat ; AbstractDirectDebitXmlMapper.java:85,152 mappent le montant brut dans CtrlSum/InstdAmt du pain.008). Toutefois le mécanisme allégué est erroné : l'exemple même du constat (33,33 % de 1000,10 − 210,07) donne exactement 123,26333 — pas d'artefact, et aucun décalage au centime n'est dérivable puisque rien n'arrondit au centime. Échecs réellement dérivables : 1000,10 réparti 33,33/33,33/33,34 somme à 1000,10000000000004 > max, déclenchant DirectDebitServiceImpl.java:272 et marquant les items ERROR ; des chiffres d'artefact (333,43334000000004) atterrissent sans arrondi dans InstdAmt SEPA, violant la règle EPC à 2 décimales. Défaut réel ; la sévérité faible tient.
Le gel d'indexation des passoires thermiques (loi Climat et Résilience) est décidé avec des seuils qui mélangent l'ancienne et la nouvelle échelle du DPE. Résultat : des logements en classe E (émissions entre 57 et 70), légalement indexables, voient leur loyer gelé — une perte de revenus pour les propriétaires concernés depuis 2022. À l'inverse, un logement pile à la limite de la classe F côté consommation d'énergie échappe au gel et subit une hausse de loyer illégale. Le test de contrôle interne valide lui-même la mauvaise échelle, donc l'erreur passe inaperçue.
Les seuils de gel d'indexation liés au DPE mélangent l'ancienne et la nouvelle échelle DPE : un seuil GES de 56 au lieu de 70 gèle à tort les logements de classe E
Le gel d'indexation F/G de la loi Climat-Résilience est décidé par BadEnergyClassAssessor à partir de limites codées en dur : 331 kWh (énergie) et 56 (GES). Selon la classification DPE 2021 applicable, la classe F débute à une énergie > 330 kWh/m²/an OU un GES > 70 kgCO2/m²/an ; 56 est le plancher de la classe F de l'ancienne échelle GES antérieure à juillet 2021. Résultat : les logements ayant un GES de 57 à 70 (classe E, légalement indexables) voient leur loyer gelé — perte de revenu pour tous les bailleurs concernés depuis 2022. En sens inverse : l'énergie utilise un `> 331` strict, de sorte qu'un logement à exactement 331 ou 330,5 kWh (classe F) échappe au gel et subit une augmentation de loyer illégale. Le test unitaire encode la mauvaise échelle : (énergie 300, gaz 60) est affirmé « bad ». De plus, EnergyRuleStartAssessor utilise un isAfter(2022-08-24) strict, excluant les contrats signés exactement à la date d'entrée en vigueur.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentCalculationDiagnosticsInfo.java:19adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/energy/BadEnergyClassAssessor.java:17adb/adb-model/src/test/java/fr/lifeconnect/adb/model/contract/rule/impl/energy/BadEnergyClassAssessorTest.java:29adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/energy/EnergyRuleStartAssessor.java:31
Contre-vérification — RentCalculationDiagnosticsInfo.java:19-21 code en dur 331/56 — l'échelle DPE antérieure à 2021 ; le DPE 2021 (art. L.173-1-1 CCH, référencé par la loi 89-462 art. 17-1) fixe la classe F à >330 kWh OU >70 kg GES. isGasClassBad() (ligne 42) gèle les GES 57-70 (classe E) ; le grep confirme l'absence de seuil corrigé ailleurs. ForceIndexationRule ne peut pas passer outre : son EnergyDiagnosticsNotExistAssessor échoue lorsque les diagnostics existent. EnergyClassAction fixe isIndexed=false, consommé par RentAmountAction:31. Le test ligne 28 (300,60→true) fige la mauvaise échelle. Le >331 strict laisse aussi s'indexer un logement à exactement 331 (classe F). Les DPE de l'ancienne échelle ont expiré le 31/12/2024, donc en 2026 le gel erroné est sans ambiguïté.
Semble un problème potentiellement valide. Les seuils DPE codés en dur devraient être revus au regard de la classification légale actuelle ; le seuil GES peut provenir d'une ancienne échelle DPE et geler à tort l'indexation pour certains biens. Les valeurs exactes doivent être confirmées côté légal/métier avant modification.
La fréquence de révision d'un bail commercial (par exemple triennale, comme le prévoit la loi) est bien saisie et rendue obligatoire à la création du contrat, mais elle n'est ensuite jamais prise en compte : le moteur de révision applique systématiquement une révision annuelle à tous les contrats, et le traitement automatique s'applique aussi aux baux commerciaux et civils à chaque publication d'indice. Un bail commercial à révision triennale sans clause d'échelle mobile subit donc trois hausses cumulées là où une seule est due. C'est une sur-facturation systématique des locataires commerciaux, avec un risque de remboursement et de contentieux.
La fréquence de révision des baux commerciaux (triennale, art. L145-38) est saisie et validée mais jamais utilisée — le moteur code en dur une révision annuelle pour tous les contrats
IndexFrequencyRule rend `indexFrequency` obligatoire pour les contrats commerciaux en reprise de gestion, et le champ est persisté (ContractEntity.indexFrequency), mais aucun code ne le lit jamais : le moteur de révision avance toujours d'exactement un an — InseeIndexKey.nextKey() renvoie (même trimestre, année+1) et prolongateNextExpectedIndexData()/calculateNextReviewDate() ajoutent un an. IndexServiceImpl cible aussi les catégories COMMERCIAL et CIVIL pour un traitement automatique à chaque publication d'indice. Un bail commercial avec révision triennale contractuelle (sans clause d'échelle mobile annuelle) est donc indexé chaque année : trois augmentations qui se composent là où une seule est due — surfacturation systématique des locataires commerciaux que le champ obligatoire prétend gérer.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/IndexFrequencyRule.java:18adb/adb-model/src/main/java/fr/lifeconnect/adb/model/index/InseeIndexKey.java:43adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1608adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:61
Contre-vérification — Re-dérivé de bout en bout : IndexFrequencyRule.java:16-22 rend indexFrequency obligatoire, or le grep ne montre aucun lecteur en logique métier ; InseeIndexKey.java:42-44 nextKey()=année+1 et NextExpectedIndexDataCreator.java:14-28 ajoutent toujours un an ; IndexFunction.java:35-54 (SQS onIndexPublished) invoque processNewInseeIndex avec la stratégie d'application automatique ProcessOnIndexPublishedStrategy (IndexServiceImpl.java:457-480), qui apparie les contrats COMMERCIAL par nextExpectedIndexKey (ContractRepository.java:142-152). Les reprises commerciales ne peuvent omettre les données d'indice (le validateur impose l'indice de base/courant). Bonus : adb-ui contracts.service.ts:224,661 code en dur FREQUENCY.YEARLY — le triennal est non représentable. Sévérité élevée maintenue (surfacturation par composition automatique pour les baux sans clause d'échelle mobile ; la plupart des baux institutionnels en ont une, donc pas critique).
indexFrequency est un champ hérité ; je ne sais pas où il est renseigné ni s'il est encore utilisé. La logique d'indexation actuelle est annuelle — vraisemblablement lié à la priorité donnée aux contrats de reprise, où la plupart des nouvelles fonctionnalités ont d'abord été implémentées pour les flux TAKE_OVER. À traiter comme une limitation de périmètre / un décalage legacy ; si les baux commerciaux exigent d'autres fréquences de révision, cela nécessite une analyse métier distincte.
Le mécanisme censé appliquer le plafonnement légal de la hausse des loyers à 3,5 % (loi d'août 2022, applicable de mi-2022 à début 2024) est désactivé en permanence : il ne s'est jamais déclenché. C'est aussi le seul endroit où un plafond spécifique Corse/Outre-mer ou ILC petites entreprises pourrait être appliqué, car ces plafonds ne sont pas intégrés dans les indices publiés par l'INSEE. Concrètement, des révisions de loyer ont pu dépasser le plafond légal sur cette période, ce qui expose la société à des demandes de remboursement de trop-perçu et à un risque juridique vis-à-vis des locataires. Même réactivé tel quel, le mécanisme planterait : il est doublement hors d'usage.
La règle de plafonnement des loyers (« bouclier loyer » / plafonnement à 3,5 %) est désactivée en permanence et inutilisable
PurchasePowerRule — le seul mécanisme mettant en œuvre le plafond de la loi 2022-1158 sur la variation de l'indice (3,5 % en métropole du T3 2022 au T1 2024 ; c'est aussi le seul endroit où un plafond Corse/DOM ou ILC-PME pourrait être appliqué, ces plafonds n'étant PAS intégrés dans les valeurs publiées par l'INSEE) — a DisableRuleAssessor comme premier assesseur, qui renvoie toujours false, de sorte que le plafond ne se déclenche jamais. Il est doublement mort : le contexte d'indexation en production construit dans RentalContract.indexation() ne renseigne jamais PURCHASE_POWER_PERIOD_KEY (seul le @Deprecated AbstractRentAmountRulesDataContext le faisait), et s'il était réactivé, PurchasePowerAction planterait : il place une List<Quantity> dans CALCULATED_AMOUNT_KEY que RentalContract.indexation caste en EffectiveRentAmount → ClassCastException. Tout bail commercial indexé sur l'ILC d'une PME révisé pendant les trimestres plafonnés a été sur-indexé. Les tests vérifient la désactivation (RuleFactoryTest:72).
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/purchase/PurchasePowerRule.java:17adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/common/assessor/DisableRuleAssessor.java:12adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/purchase/PurchasePowerAction.java:22adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1525
Contre-vérification — Re-dérivé : PurchasePowerRule.java:17 place DisableRuleAssessor en premier (renvoie false, DisableRuleAssessor.java:12) et AbstractRule.apply() court-circuite, donc l'action ne s'exécute jamais ; RuleFactoryTest.java:72 le fige. Le contexte en production (RentalContract.java:1514-1520) ne définit jamais PURCHASE_POWER_PERIOD_KEY ; le seul renseigneur AbstractRentAmountRulesDataContext est @Deprecated/abstrait sans sous-classe. S'il était réactivé, PurchasePowerAction.java:20-24 place une List<Quantity> là où RentalContract.java:1525 caste en EffectiveRentAmount → CCE. Aucun autre mécanisme de plafonnement n'existe (grep bouclier/plafonnement/PURCHASE_POWER). Sévérité abaissée : la fenêtre de plafonnement s'est terminée au T1 2024, le plafond IRL métropole était intégré aux valeurs INSEE publiées, et le comportement pré-monorepo pendant la fenêtre est inconnaissable — l'exposition se limite aux baux historiques Corse/DOM et ILC-PME.
Intentionnel. La règle a été désactivée à la demande de Pierre ; le fait que le plafond ne s'applique pas actuellement est donc un comportement attendu, pas une implémentation manquante.
Quand une révision de loyer échoue en cours de route, l'erreur est absorbée en silence alors que le contrat a déjà été partiellement modifié : la date de prochaine révision et l'indice attendu ont été avancés, et certaines lignes de loyer ont pu être révisées, d'autres non. Cet état à moitié modifié est ensuite enregistré tel quel. Le contrat indique donc que la révision a été faite alors que le loyer n'a pas (ou pas entièrement) été augmenté : la révision manquée ne sera jamais reproposée, et personne n'est alerté — perte de revenus discrète pour le propriétaire.
L'échec d'indexation est silencieusement avalé après une mutation en place du contrat, puis l'état à moitié muté est persisté
RentAmountServiceImpl.indexation se termine par `.onErrorReturn(rc)`. Mais au moment où une erreur survient (par ex. « Current rent amount not found » levée à RentalContract.java:1508-1512, ou un échec de checkForSameMonthEffectiveDate sur le second RentAmount après que le premier a déjà été upserté), rc a déjà été muté en place : prolongateNextExpectedIndexData() a avancé nextExpectedRentReviewDate/nextExpectedIndexKey, et certains RentAmounts peuvent être partiellement indexés. L'appelant (IndexServiceImpl.updateContractWithNewIndex:378-392) persiste alors les rentAmounts de rc, la liste d'indices enrichie (nouvel indice enregistré) et les données de prochaine révision avancées. Effet net : la comptabilité d'indice du contrat indique que la révision a eu lieu alors que le loyer n'a jamais été (ou seulement partiellement) indexé — la révision de l'année est silencieusement perdue et le cycle suivant attend l'indice de l'année d'après.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/RentAmountServiceImpl.java:201adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:378adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1508
Contre-vérification — Re-dérivé : RentAmountServiceImpl.java:191-201 appelle rc.prolongateNextExpectedIndexData() (setters en place, RentalContract.java:1598-1611) avant rc.indexation(), qui peut lever à RentalContract.java:1508 (« Current rent amount not found ») ou via RentAmount.upsertAmount → checkForSameMonthEffectiveDate (RentAmount.java:39,162-180) après que des RentAmounts antérieurs ont été mutés en place. onErrorReturn(rc) (ligne 201) renvoie silencieusement le contrat à moitié muté ; IndexServiceImpl.java:378-392 + ProcessOnIndexPublishedStrategy (466-475) persistent les rentAmounts non indexés avec le nouvel indice enregistré et nextExpectedIndexKey/ReviewDate avancés. addIndexIfNotExists/isIndexAdded (323-335, 364) empêchent toute nouvelle tentative, donc la révision est définitivement perdue sans aucun log. Sévérité moyenne maintenue : le déclenchement nécessite des données anormales, mais la perte est silencieuse et irréversible.
Problème valide. Si une erreur survient pendant l'indexation après que certains montants de loyer ont déjà été modifiés, .onErrorReturn(rc) peut laisser un objet partiellement mis à jour poursuivre le flux. L'indexation devrait être atomique : soit tous les montants sont mis à jour avec succès, soit l'opération échoue sans persister de changement partiel.
Quand une révision se termine en erreur sans montant calculé (cas typique : logement sans diagnostics DPE enregistrés et sans dérogation activée par le gestionnaire), le système enregistre malgré tout une ligne de loyer à zéro euro marquée en erreur. La facturation, elle, est protégée : elle ignore ces lignes en erreur. Mais le calcul de la révision suivante ne les ignore pas : il prend la ligne la plus récente comme base, y compris cette ligne à zéro. La prochaine indexation peut donc être calculée à partir d'un loyer de zéro euro, aboutissant à un loyer révisé faux — uniquement dans ce scénario où une révision précédente a échoué de cette manière.
L'indexation suivante se compose à partir d'une ligne de loyer ERROR à montant nul, parce que l'indexation utilise le finder non filtrant
Lorsque la chaîne de règles se termine en ERROR sans montant calculé (cas typique : appartement/maison d'habitation sans fichiers DPE et forceIndexReview non défini — ForceIndexationAction fixe STATE=ERROR, RentAmountRule est sauté), ErrorStateAction fabrique un EffectiveRentAmount avec amount = Quantity.ZERO, status ERROR, whenEffective = date de révision, et RentalContract.indexation l'upserte dans le contrat. La facturation est protégée (effectiveRentAmount() filtre isErrorStatus et evaluateStatus signale la période de quittancement), mais pas la base d'indexation : RentalContract.indexation ligne 1508 utilise RentAmount.findCurrentAmount(), qui ne filtre PAS les montants ERROR. À la révision suivante, le montant le plus récent est la ligne ERROR à zéro, donc applyIndex multiplie 0 et upserte un loyer de 0,00 avec status SUCCESS — qui devient alors facturable. Une année ERROR non résolue se transforme silencieusement en un loyer permanent de 0 € l'année suivante.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/error/ErrorStateAction.java:44adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentAmount.java:86adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1508
Contre-vérification — Re-dérivé de bout en bout. ErrorStateAction.java:43 crée la ligne INDEXATION ZERO/ERROR (vérifiée par test dans RentAmountIndexerTest:208-212) ; RentalContract.java:1508 fonde la révision suivante sur findCurrentAmount, qui n'a pas le filtre isErrorStatus (RentAmount.java:86-91 vs :74). Les garde-fous échouent : resetUnusedIndexedAmount ne s'exécute que via les messages de fichiers de diagnostics (RentAmountServiceImpl:258, FileMetadataFunction) ; le onFutureIndexReCalculationCallBack de ContractFunction n'est jamais branché à un listener, donc patcher forceIndexReview ne recalcule jamais ; IndexesHelper:114 traite la ligne à zéro comme « indice futur appelé », bloquant les ré-exécutions. L'indexation de l'année 2 produit une ligne facturable de 0 € (WARNING via le chemin forceIndexReview, SUCCESS si le DPE est chargé après la rotation d'indice). Sévérité moyenne maintenue.
Non considéré comme un bug. Les montants de loyer en erreur sont stockés intentionnellement à des fins d'historique et de diagnostic ; les méthodes de RentAmount utilisées pour le calcul du loyer effectif ne sélectionnent que les entrées valides et excluent les états d'erreur.
Le calcul de la prochaine date de révision compare uniquement les années civiles, sans regarder le mois. Pour un contrat dont les révisions ont pris du retard, si le traitement tourne avant la date anniversaire de l'année en cours, il programme directement la révision de l'année suivante — sautant une révision encore parfaitement réclamable, soit une année entière d'indexation abandonnée. En plus, la date de révision et le millésime de l'indice attendu se désynchronisent de deux ans : la révision finalement appliquée est calculée sur un indice périmé.
getNextReviewYear ne compare que les années civiles — saute une révision légalement réclamable et désynchronise la date de révision de la clé d'indice attendue
NextExpectedIndexDataCreator.getNextReviewYear saute à now.year+1 dès que now.year - lastReviewYear > 1, en ignorant le mois/jour. Exemple : contrat débuté le 2024-05-01 dont les révisions ont calé ; le job s'exécute le 2026-03-01 (l'anniversaire du 2026-05-01 est encore à venir et réclamable) → diff=2 → prochaine révision fixée au 2027-05-01, sautant la révision réclamable de 2026 (une année entière d'indexation perdue). Parallèlement, setupNextExpectedIndexData fixe nextExpectedIndexKey = currentIndex.nextKey() (année de base+1, par ex. trimestre 2025), de sorte que le contrat devient candidat à la publication de l'indice 2025 mais l'applique avec whenEffective 2027 — la date de révision et le millésime d'indice divergent de deux ans, produisant une révision calculée sur un ratio d'indice périmé à la mauvaise date d'effet.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/NextExpectedIndexDataCreator.java:23adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1587
Contre-vérification — Re-dérivé : getNextReviewYear (NextExpectedIndexDataCreator.java:23-28, dupliqué dans IndexesHelper.java:124-129) utilise une différence sur l'année seule, donc lastReview 2024-05-01 avec now=2026-03-01 donne 2027-05-01, sautant l'anniversaire encore réclamable du 2026-05-01. Parallèlement, setupNextExpectedIndexData (RentalContract.java:1587-1591) et prolongateNextExpectedIndexData (1598-1609) n'avancent nextExpectedIndexKey que d'un an (InseeIndexKey.nextKey:42-44), désynchronisant date et millésime. Atteignable : le fallback de setup se déclenche à la saisie rétroactive d'un contrat hors reprise (ContractServiceImpl:996/1006, sans garde sur la date de début) ; la prolongation se déclenche sur une application calée depuis ≥2 ans (RentAmountServiceImpl:192). Les tests ne couvrent que des entrées à l'anniversaire exact. Règle art. 17-1 correctement énoncée. Sévérité moyenne maintenue : précondition limite, perte monétaire silencieuse.
Nécessite une clarification métier. Le but de la méthode est de calculer une date de révision future, pas de créer immédiatement une révision à la date du jour — le comportement actuel peut être intentionnel. Si le métier attend que les révisions manquées soient rattrapées, le calcul devrait prendre en compte la date complète et pas seulement l'année.
Le contrôle qui vérifie que l'indice choisi correspond au type de bail ne couvre que les contrats repris d'une gestion antérieure et les baux d'habitation. Un bail commercial ou civil créé directement dans le système n'est soumis à aucun contrôle : on peut lui affecter l'IRL — ou n'importe quel indice — sans blocage. Par ailleurs, pour les baux commerciaux contrôlés, le système accepte l'ICC (indice défendable seulement pour d'anciens contrats repris, mais incorrect pour les baux conclus ou renouvelés après septembre 2014) alors que le message d'erreur affiché annonce que seuls l'ILAT et l'ILC sont admis : la règle appliquée et la règle annoncée se contredisent. Le risque : des révisions calculées sur le mauvais indice, donc des loyers contestables.
La validation du type d'indice n'a aucune branche pour les baux commerciaux/civils hors reprise ; la branche commerciale accepte silencieusement l'ICC alors que son message d'erreur ne mentionne qu'ILAT/ILC
BaseIndexRule/CurrentIndexRule ne valident le type d'indice que pour (commercial ET reprise), (civil ET reprise) ou les contrats loi 89. Un bail commercial ou civil créé dans le système (non repris en gestion) ne rencontre aucune branche, de sorte que l'IRL — ou n'importe quel type — est accepté pour un bail commercial. De plus, isValidCommercialRentalContractIndex accepte l'ICC (défendable uniquement pour les reprises héritées pré-Pinel, incorrect pour les baux conclus/renouvelés après le 01/09/2014) alors que son message d'exception ne liste que « ILAT ou ILC » : l'ensemble accepté et la règle communiquée se contredisent.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/BaseIndexRule.java:41adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/CurrentIndexRule.java:46
Contre-vérification — BaseIndexRule.java:41-48 et CurrentIndexRule.java:45-53 n'ont aucune branche pour les baux commerciaux/civils hors reprise ; RentalContractValidatorFactory.java:12-20 envoie tous les contrats en CREATION vers NewRentalRentalContractValidator (uniquement ces deux règles), donc le type d'indice n'est pas vérifié. Pire, adb-ui rc-conditions.component.ts:370-385 remplit le menu déroulant uniquement avec l'IRL (getIndexesIRL, ligne 210) pour tout nouveau bail et setIndexes() assigne automatiquement indexList[0], de sorte que les nouveaux baux commerciaux reçoivent silencieusement l'IRL. L'ICC est accepté alors que le message dit « ILAT ou ILC », vérifié à BaseIndexRule.java:64-68, CurrentIndexRule.java:69-76. Règle légale (loi Pinel, L145-34/L145-38) correctement énoncée. Aucun test ni autre garde trouvé.
Vraisemblablement lié au périmètre de la livraison initiale. La priorité étant de démarrer vite avec les contrats TAKE_OVER, la plupart des nouvelles règles de validation ont été implémentées pour les flux de reprise. La couverture des contrats hors reprise peut nécessiter une analyse métier supplémentaire.
Le gel des loyers pour passoire thermique ne s'applique que si les deux valeurs du DPE (consommation d'énergie et émissions de gaz) sont renseignées. Si une seule manque — par exemple une consommation de classe G bien enregistrée mais pas d'émissions — le système considère qu'il n'y a pas de diagnostic du tout. Le gestionnaire peut alors forcer l'indexation d'un simple clic, et le loyer est augmenté malgré une valeur classe G présente au dossier, sans aucun avertissement. Le comportement par défaut bloque au moins la révision ; le risque n'existe que si le gestionnaire active la dérogation.
Gel DPE contournable avec des diagnostics partiels : le chemin de forçage d'indexation traite une seule valeur manquante comme « aucun diagnostic »
EnergyClassRule ne se déclenche que lorsque EnergyDiagnosticsExistAssessor passe, ce qui exige que energyClassKWH ET gasClassKWH soient tous deux non nuls. Un logement ayant une valeur énergétique enregistrée de par ex. 450 kWh (classe G) mais sans fichier GES bascule vers ForceIndexationRule (EnergyDiagnosticsNotExistAssessor = négation du même test « les deux »). Si le gestionnaire définit forceIndexReview = WITH_NEW_INDEX, le loyer est indexé malgré une valeur énergétique de classe G au dossier — le gel légal est contourné d'un seul clic, et rien n'avertit qu'un des deux diagnostics est présent et disqualifiant. (Par défaut, UNDEFINED déclenche au moins une erreur.)
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/energy/EnergyDiagnosticsExistAssessor.java:27adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/indexation/forceindex/ForceIndexationAction.java:37
Contre-vérification — Re-dérivé : DiagnosticsInfoServiceImpl.java:52-60 fixe par défaut chaque valeur manquante à 0 ; EnergyDiagnosticsExistAssessor.java:27 exige que les DEUX soient non nulles, donc énergie=450/gaz=0 saute EnergyClassRule (AbstractRule court-circuite), tandis que ForceIndexationRule (conditionnée par la négation) se déclenche et ForceIndexationAction.java:37-38 fixe IS_INDEXED=true pour WITH_NEW_INDEX ; patchForceIndexReviewInfo n'a aucun garde. Juridiquement correct : la classe DPE est la moins bonne des deux échelles, donc 450 kWh seul est classe G (le gel s'applique). Des mitigations existent — le défaut UNDEFINED déclenche une erreur, un avertissement rouge dans l'UI via isPartDiagnosticsDataExists (AggregateContractServiceImpl.java:213-220), status WARNING (RentAmountAction.java:52) — mais aucune ne fait ressortir la valeur disqualifiante. Sévérité faible justifiée.
Intentionnel. La règle exige des diagnostics DPE complets avant d'appliquer le gel ; si une seule valeur de diagnostic est fournie, le système considère l'information insuffisante pour bloquer l'indexation.
Le rapport entre l'ancien et le nouvel indice est calculé avec une arithmétique approximative au lieu d'un calcul décimal exact. Dans certains cas limites d'arrondi (résultat tombant exactement sur un demi-centime, fréquent avec des indices et loyers à deux décimales), le loyer révisé sort avec un centime de moins que le calcul de référence ANIL/INSEE. L'écart est minime mais il se cumule d'année en année sur chaque ligne de loyer, et un locataire ou un contrôle qui refait le calcul officiel trouvera un montant différent.
Ratio d'indice calculé en double précision au lieu d'une division exacte en BigDecimal
applyIndex extrait les deux valeurs d'indice via BigDecimal::doubleValue, les divise comme des doubles primitifs, et multiplie le loyer par ce facteur double (ré-encapsulé avec BigDecimal.valueOf puis HALF_UP à 2 décimales). Le calcul légalement publié est loyer × nouvelIndice / ancienIndice en arithmétique décimale exacte puis arrondi. Lorsque le produit exact tombe exactement sur un demi-centime (fréquent avec des indices et loyers à 2 décimales, par ex. des produits se terminant par .005), la représentation en double du ratio peut se situer quelques ULP sous la valeur réelle, de sorte que HALF_UP arrondit vers le bas là où l'arithmétique exacte arrondit vers le haut — un écart de 1 centime par rapport au calcul de référence ANIL/INSEE, par ligne de loyer, qui se compose d'année en année puisque chaque révision alimente la suivante.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1545adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/Quantity.java:161
Contre-vérification — RentalContract.java:1534-1546 convertit les deux valeurs d'indice via doubleValue et divise comme des doubles ; Quantity.java:161-162 multiplie par BigDecimal.valueOf(double) puis HALF_UP à 2 décimales. Le chemin est actif : RentAmountAction.java:39 appelle applyIndex pendant l'indexation, et applyIndexToCallForRent (RentalContract.java:1612) alimente les documents contractuels. Une simulation sur 2 M de triplets IRL/loyer réalistes à 2 décimales a reproduit ~129 divergences d'un centime vs loyer×nouveau/ancien exact (par ex. 950,25×110,76/109,20=963,825 exact → 963,83, mais le ratio double donne 963,82). Rare (~0,006 % des révisions), 1 centime chacun, donc sévérité faible correcte ; la « composition » est réelle mais négligeable.
Quand la régularisation annuelle de charges est en faveur du locataire (provisions supérieures aux dépenses réelles), le gestionnaire saisit un avoir qui vient solder le prochain appel de charges. Le système traite alors cet appel comme s'il avait été payé en espèces et crédite le propriétaire du montant correspondant, alors qu'aucun argent n'est entré et que c'est au contraire le locataire qui doit être remboursé. Résultat : le compte du propriétaire est gonflé d'une somme fictive, ses relevés de gérance (CRG) sont faux et l'agence risque de reverser de l'argent qu'elle n'a jamais encaissé. Le contrôle censé réserver ce mécanisme aux vrais encaissements existe mais est inopérant.
Des crédits manuels non monétaires (ex. régularisation de charges en faveur du locataire) déclenchent un virement TenantToOwner de type trésorerie qui crédite le propriétaire
isEligibleEntry() vise à restreindre les écritures au crédit susceptibles de déclencher un virement au propriétaire après lettrage : uniquement de la trésorerie réelle (TENANT_PAYMENT_RECEIVED, SUPPLIER_PAYMENT_ISSUED) plus les écritures manuelles de natures reprise-trésorerie/GLI. Mais MANUAL_ENTRY figure aussi dans ALLOWED_CREDIT_EVENT_TYPE, ce qui rend la liste des natures du code mort. Scénario : les provisions dépassent les charges réelles ; l'opérateur passe un RENTAL_CHARGES_ADJUSTMENT en direction TENANT_PAYABLE (débit 706200 / crédit 411100/RC, montant R) et le lettre contre le débit du prochain CALL_FOR_RENTAL_CHARGES. L'appel lettré déclenche TenantToOwner : débit 800000 / crédit 466100 pour R — le propriétaire est crédité comme si de la trésorerie était arrivée, alors qu'il doit en réalité R au locataire (aucun débit propriétaire n'est jamais passé). Le CRG du propriétaire est surévalué jusqu'à 2R et la banque de l'agence décaisse de l'argent jamais encaissé. ManualEntryEventTest va jusqu'à valider ce virement aveugle à la direction.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:202adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForRentalCharges.java:130adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:236adb-accounting/src/test/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEventTest.java:93
Contre-vérification — Re-dérivé : PostSettlementsProcessorImpl.java:49-53 place MANUAL_ENTRY dans ALLOWED_CREDIT_EVENT_TYPE, si bien que isEligibleEntry (:201-206) laisse passer n'importe quel crédit manuel ; le filtre par nature (:54-60,203-204) est du code mort. RentPostingRuleFactory.java:60-76,819-834 montre que l'ajustement TENANT_PAYABLE crédite le locataire 411 / débite 706200 sans aucun débit propriétaire ; SettlementServiceImpl n'impose aucune limite de type d'événement au lettrage ; CallForRentalCharges.java:130 émet ensuite TenantToOwner, et PostingRuleFactory.java:236-250 crédite le propriétaire (466100) alors qu'aucune trésorerie n'a été reçue — en contradiction avec l'intention de tenant-to-owner.md ("somme reçue du locataire"). Aucun test ne garde le filtre. Sévérité élevée confirmée.
Problème valide. MANUAL_ENTRY doit être retiré de ALLOWED_CREDIT_EVENT_TYPE. Les crédits manuels génériques ne seraient alors plus traités comme des événements de trésorerie, tandis que les natures d'écriture manuelle spécifiquement autorisées continueraient de passer par le filtre de nature dédié.
Le système ne calcule jamais la régularisation annuelle de charges : il ne compare pas les provisions appelées aux dépenses réelles de l'immeuble, ne sait pas distinguer les charges récupérables des non récupérables (décret 87-713), et ne prorate pas selon la durée d'occupation du locataire. Le gestionnaire doit tout calculer hors système (tableur) puis saisir le résultat manuellement, alors que la fonctionnalité est annoncée comme prévue dans la documentation. Cela représente une charge de travail manuelle importante, un risque élevé d'erreurs sur les montants réclamés ou remboursés aux locataires, et une exposition juridique si des charges non récupérables leur sont facturées.
Aucun calcul de régularisation annuelle des charges : réel vs provisions, classification décret 87-713 et prorata d'occupation sont entièrement manuels
Le cas métier documenté de RENTAL_CHARGES_ADJUSTMENT est la « régularisation annuelle de charges » (statut doc : « En cours » — seul événement non livré). Dans le code, ce n'est qu'un ManualEntryEvent nu : l'opérateur calcule le montant hors système. Rien ne compare les provisions appelées (706200 par contrat/période) avec les charges réelles récupérables de l'immeuble ; les factures fournisseurs (InvoiceItem : uniquement expenseAccount/direction/amount) ne portent aucun indicateur récupérable/non récupérable ; le modèle `Charge.isRecoverable`/`ChargeEntry.recoverableShare` n'existe que dans les budgets d'adb-parts et n'est jamais lu par adb-accounting ni adb-contracts (un grep sur recoverable/87-713/régul dans ces deux services ne renvoie rien). Aucun prorata pour les années d'occupation partielle, aucun recalcul après départ, aucun contrôle de prescription triennale. Idem pour l'eau (aucun calcul de compteur — aucun compteur/meterReading nulle part) et la TEOM.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/RentPostingRuleFactory.java:60adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/charges/Charge.java:39adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.java:35adb-doc/docs/concepts/accounting/events/rental-charges-adjustment.md:36
Contre-vérification — Écart re-dérivé : RENTAL_CHARGES_ADJUSTMENT est typé par l'opérateur (adb-ui entry-details.component.ts:85 → RentalContract.java:1165 passe event.getAmount() tel quel → RentPostingRuleFactory.java:60 manualEntry). La doc rental-charges-adjustment.md:36 indique le statut « En cours »/non pleinement livré. Charge.isRecoverable n'existe que dans adb-parts (CustomChargeRepositoryImpl.java:122 le projette) sans aucun consommateur dans adb-accounting/adb-contracts ; InvoiceItem.java n'a aucun indicateur de récupérabilité. Des greps FR+EN exhaustifs (régularis, récupérable, 87-713, décompte, compteur, TEOM, prescription, tantième) sur tous les services et les deux frontends n'ont trouvé aucun moteur de calcul ; le prorata en RentalContract.java:869 ne concerne que l'appel de loyer. Sévérité élevée confirmée.
Intentionnel, pas une régression. RENTAL_CHARGES_ADJUSTMENT a été implémenté comme un flux manuel : le gestionnaire calcule le montant de régularisation hors système et crée l'écriture manuelle correspondante. La comparaison automatique charges réelles vs provisions, le prorata d'occupation et la classification légale ne faisaient pas partie de l'implémentation actuelle — introduits, de mémoire, comme un processus MVP rapide pour préparer les données avant la mise en production.
Pour les contrats facturés au trimestre, au semestre ou à l'année, si la période d'occupation commence et se termine toutes deux en cours de mois, le calcul ne retient que le dernier mois incomplet et oublie le premier. L'appel de loyer et l'appel de provisions de charges sont alors trop faibles : sur un bail annuel occupé du 15 janvier au 20 novembre, il manque environ un demi-mois de loyer et de charges. La facturation mensuelle classique n'est pas concernée, mais chaque contrat facturé sur des périodes longues avec deux mois partiels génère un manque à gagner pour le propriétaire, invisible sans recomptage manuel.
proratedMonth() ne conserve que le dernier mois partiel, sous-facturant loyer et provisions de charges sur les périodes comportant deux mois partiels
AbstractContractPeriod.proratedMonth() parcourt les mois et ÉCRASE ratio[0] à chaque mois partiel au lieu de l'accumuler. Toute période de facturation (rentPaymentBasisPeriod trimestriel/semestriel/annuel) qui commence ET se termine en milieu de mois contient deux mois partiels ; le premier partiel est silencieusement perdu. Exemple : contrat facturé annuellement, occupation du 15 janv. au 20 nov. : fullMonths()=9 (fév.–oct.), proratedMonth() renvoie 20/30 (nov.) et ignore le 17/31 de janvier → CALL_FOR_RENT et CALL_FOR_RENTAL_CHARGES pour la période manquent d'environ 0,55 mois. calculatePeriodAmounts (lignes 868-871) additionne fullMonthsRent + un seul proratedRent. La facturation mensuelle n'est pas affectée, d'où le fait que RentalContractPeriodTest (cas mensuels uniquement) passe.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1715adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:869
Contre-vérification — proratedMonth() (RentalContract.java:1715-1725) affecte ratio[0] par mois partiel ; durations() (:1727-1743) émet un segment par mois calendaire, si bien qu'une période de paiement commençant ET se terminant en milieu de mois produit deux partiels dont le premier est perdu. calculatePeriodAmounts (:868-871) n'ajoute que fullMonths + un seul proratedMonth. Atteignable : le premier appel utilise begin=getBegin() et end=getEnd(), tous deux des jours arbitraires (:588-589, 946-952, 1484-1486) ; en outre split() sur un futureRent en milieu de mois (:807-809) crée une sous-période [milieu de mois, milieu de mois]. Rejeu numérique (15 janv.–20 nov., annuel) : 9,667 mois facturés contre 10,215. Les tests (helpers trimestriels/annuels) ne fixent jamais de date de fin, donc seuls les cas à partiel unique sont vérifiés. L'affirmation de la description sur les « cas mensuels uniquement » du test est erronée, mais le défaut tient. Sévérité moyenne correcte : sous-facturation réelle, déclencheur étroit (facturation non mensuelle + deux partiels dans une même période).
Problème valide, déjà confirmé aux revues quittancement et arrondis. Le ratio de mois partiel est écrasé au lieu d'être cumulé, ce qui peut sous-estimer à la fois le loyer et les provisions de charges quand une période commence et se termine sur des mois partiels.
Le système distingue bien les baux avec provisions sur charges des baux au forfait de charges, mais cette distinction ne sert qu'à l'indexation. Rien n'empêche un gestionnaire de saisir une régularisation annuelle de charges sur un bail au forfait — meublé ou colocation par exemple — alors que la loi l'interdit : le forfait exclut toute régularisation. Une telle écriture est juridiquement nulle et expose le propriétaire mandant à des réclamations et remboursements de la part du locataire. C'est un garde-fou réglementaire qui repose aujourd'hui uniquement sur la vigilance du gestionnaire.
Le forfait de charges (LUMPSUM) ne bloque pas la régularisation des charges — la distinction est modélisée mais jamais appliquée
RentalContract.initialRentChargesType distingue PROVISION et LUMPSUM, mais son seul consommateur fonctionnel est l'éligibilité à l'indexation (les charges au forfait sont indexées avec le loyer — correct au regard de l'art. 8-1). Rien n'empêche de passer un RENTAL_CHARGES_ADJUSTMENT (ou un appel de charges ponctuel) sur un contrat au forfait : RentalContract.manualEntry() (lignes 1160-1171) dispatche uniquement sur la nature de l'événement, sans aucun contrôle de initialRentChargesType ; aucun validateur dans adb-contracts ou adb-accounting ne l'inspecte pour les événements d'ajustement. Un bail meublé/colocation au forfait peut ainsi recevoir une régularisation annuelle de charges, ce qui est légalement nul (le forfait exclut la régularisation) et expose le mandant à des réclamations du locataire.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1165adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/impl/indexation/eligibility/IndexationEligibilityAction.java:25adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.java:181
Contre-vérification — Re-dérivé de façon indépendante. RentalContract.java:1164-1171 passe RENTAL_CHARGES_ADJUSTMENT sur la seule nature de l'événement ; un grep exhaustif montre que les seuls consommateurs fonctionnels de initialRentChargesType sont l'éligibilité à l'indexation (IndexationEligibilityAction.java:25, IndexesHelper.java:121) plus un contrôle de présence non nulle (RentalContractReadyToSignValidator.java:181). Les validateurs de ManualEntryEvent.java:220-246 ne contrôlent que direction/nature/période, jamais le régime de charges du contrat ; AccountingEventController n'a aucun garde-fou au niveau du contrat. L'UI entry-details.component.ts:85 propose RENTAL_CHARGES_ADJUSTMENT sans condition. Règle légale (art. 8-1 V / 25-10 : le forfait exclut la régularisation) correctement énoncée. Sévérité moyenne confirmée : requiert une action de l'opérateur, mais rien n'empêche l'écriture légalement nulle.
Semble être un comportement attendu plutôt qu'un bug. Le flux de régularisation est manuel et le système permet intentionnellement au gestionnaire de créer l'ajustement même pour un contrat LUMPSUM. De mémoire, ce comportement a été demandé par Pierre, mais la spécification métier d'origine devrait être vérifiée avant tout changement.
Quand la quote-part d'un locataire est calculée par pourcentage (par exemple 33,33 % de 838,60 €), le montant obtenu peut avoir plus de deux décimales (279,50538 €) et il est envoyé tel quel dans le fichier de prélèvement SEPA. Les règles bancaires n'acceptent que deux décimales en euros : la banque peut rejeter le fichier entier, bloquant l'encaissement de tous les prélèvements du lot. De plus, le montant réellement prélevé et le montant enregistré en comptabilité peuvent différer légèrement, car ils ne sont pas arrondis de la même façon.
Montants de prélèvement SEPA calculés en double et exportés vers pain.008 avec un nombre de décimales non borné
DirectDebitItem.sepaDebitAmount() calcule la quote-part de chaque locataire par arithmétique double brute (fullAmount * rate, moins l'aide sociale) et enveloppe le résultat via Quantity.euro(double) SANS aucun arrondi à 2 décimales. La valeur est persistée telle quelle (QuantityMapper n'applique aucune mise à l'échelle) et reportée verbatim dans les champs InstdAmt et CtrlSum de l'en-tête du pain.008 SEPA. Une quote-part de 33,33 % de 838,60 donne 279,50538 — plus de 2 décimales, ce qui viole la règle SEPA/ISO 20022 sur les montants EUR (2 décimales maximum) et risque le rejet de tout le fichier de remise par la banque. Par ailleurs le paiement comptabilisé est arrondi via Quantity.toMoney() (HALF_DOWN), si bien que le montant prélevé par la banque et le montant comptabilisé au grand livre peuvent diverger. Le test unitaire tolère un écart de 0,1 EUR, entérinant l'imprécision.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitItem.java:108adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/AbstractDirectDebitXmlMapper.java:152adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/creation/impl/DirectDebitPaymentCreationStrategy.java:69adb-contracts/src/test/java/fr/lifeconnect/adb/contract/model/directdebit/DirectDebitItemTest.java:12
Contre-vérification — Redérivé de bout en bout. DirectDebitItem.java:108-127 calcule fullAmount*rate en double et enveloppe via Quantity.euro(Double) (Quantity.java:77, BigDecimal.valueOf — sans setScale). QuantityMapper persiste verbatim ; AbstractDirectDebitXmlMapper.java:85/152/232-242 reporte le montant non arrondi dans InstdAmt et CtrlSum ; le pain.008.001.02.xsd embarqué autorise fractionDigits=5 donc le marshalling ne le détecte pas, violant la règle EPC des 2 décimales EUR au niveau de la banque. Toute quote-part non 100 % avec des centimes impairs le déclenche (50 % de 100,01 = 50,005). Le Payment comptabilisé arrondit HALF_DOWN dans le constructeur Money (Money.java:39,97), divergeant du montant exporté. DELTA=0,1 du test confirmé.
Problème valide. Le montant est calculé en virgule flottante et passé à Quantity.euro(double) sans normalisation finale explicite à deux décimales avant la génération du pain.008. Le calcul devrait utiliser de l'arithmétique décimale dès que possible, et le montant SEPA exporté doit être explicitement arrondi et formaté à deux décimales.
Quand un règlement est lettré sur une dette puis que ce lettrage est annulé, la dette redevient bien ouverte dans la comptabilité. Mais les écrans de recherche des impayés comptent le lettrage et son annulation comme deux règlements au lieu de les neutraliser : un loyer de 100 € réglé puis dé-réglé apparaît comme réglé à 200 €. Résultat : ces dettes rouvertes n'apparaissent plus dans les listes d'écritures et de factures non soldées, ni dans la sélection des prélèvements à émettre. Des impayés bien réels deviennent invisibles pour les gestionnaires et risquent de ne jamais être relancés ni recouvrés.
Les agrégations Mongo calculent le montant lettré en ignorant le sens du lettrage, en contradiction avec le modèle de domaine
AccountingEntry.settledAmount() nette les lettrages par sens (crédit moins débit pour une écriture DEBIT), si bien qu'un lettrage et sa contre-passation (reverseExistingSettlements ajoute un lettrage de sens opposé, il n'en retire jamais) se nettent à zéro et l'écriture se rouvre. Les agrégations Mongo utilisées pour les requêtes de postes ouverts SOMMENT au contraire tous les montants de lettrage sans signe : une écriture de loyer de 100 EUR lettrée 100 puis contre-passée 100 obtient settledAmount=200 en agrégation, unSettledAmount=-100, et est exclue par le filtre `unsettledAmountMin`. Les dettes locataires rouvertes disparaissent donc des résultats de recherche d'écritures non lettrées et de factures non lettrées (recherche de candidats au lettrage de prélèvement, filtres de montant ouvert de facture) alors que le domaine Java les rapporte toujours ouvertes — deux autorités sont en désaccord sur ce qui reste impayé.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/aggregations/impl/AuxiliaryAccountAggregationOperations.java:112adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/AccountingEntry.java:323adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/InvoiceSearchRepositoryImpl.java:335adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/settlement/creation/impl/DirectDebitPaymentSettlementCreationStrategy.java:44
Contre-vérification — Redérivé : reverseExistingSettlements ajoute des lettrages de sens opposé (AccountingEntry.java:274,294 ; persistés par ReversalSettlementsHandler.java:61-69), donc le settledAmount Java (AccountingEntry.java:323) se nette à 0 et l'écriture se rouvre, tandis que AuxiliaryAccountAggregationOperations.java:112-116 somme sans signe, produisant unSettledAmount=-100 et l'exclusion en :67-69. Consommateurs réels : adb-ui payment.service.ts:436 (unsettledAmountMin:0), adb-contracts RentalContractAuxiliaryEntriesQuery (includeOnlyUnSettled), DirectDebitPaymentSettlementCreationStrategy.java:44. Deux réserves : DirectDebit exclut aussi les écritures rouvertes via isReversedEntry() (ligne 46), l'agrégation n'y est donc pas l'unique cause ; et l'affirmation sur les factures est exagérée — Invoice.java:357-367 somme également sans signe, donc InvoiceSearchRepositoryImpl:335 est cohérent avec son modèle de domaine. La contradiction de fond tient ; la gravité élevée est correcte.
Problème valide. Le modèle de domaine calcule un solde signé selon le sens du lettrage, alors que l'agrégation somme les montants sans appliquer ce sens. Cela devient faux quand un lettrage est contre-passé par un lettrage de sens opposé. L'agrégation devrait calculer des montants signés par sens avant de les sommer.
Quand une période de quittancement (trimestrielle, semestrielle ou annuelle) contient deux mois incomplets — par exemple un bail qui commence le 14 novembre et se termine le 20 décembre sur une base trimestrielle — seul le dernier mois partiel est facturé. Le loyer du premier mois partiel disparaît purement et simplement de l'appel de loyer et des appels de charges : sur un loyer de 1 000 €/mois, on facture environ 645 € au lieu d'environ 1 212 €. C'est du chiffre d'affaires perdu pour le propriétaire, sans aucune alerte, et les tests ne couvrent pas ce cas. Le problème ne survient que dans cette configuration précise (deux mois partiels dans la même période).
proratedMonth() ne conserve que le dernier mois partiel — loyer sous-facturé lorsqu'une période de facturation comporte deux mois partiels
AbstractContractPeriod.proratedMonth() écrase ratio[0] à chaque mois partiel au lieu de l'accumuler, tandis que fullMonths() ne compte que les mois complets. Toute période de facturation contenant DEUX mois partiels (loyer trimestriel/semestriel/annuel où le contrat commence et se termine tous deux en cours de mois à l'intérieur de la même période, p. ex. début le 14 nov., départ du locataire le 20 déc. sur une base trimestrielle) ne facture que le DERNIER mois partiel : attendu 17/30 + 20/31 du loyer mensuel (~1211,83 pour 1000/mois), réel 20/31 seulement (645,16) — le loyer du premier mois partiel disparaît silencieusement des écritures CALL_FOR_RENT / CALL_FOR_RENTAL_CHARGES / CFR_CHARGES_HOUSEHOLD_WASTE (411100→706100/706200). Les tests ne couvrent que des périodes à un seul mois partiel.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1715adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:868adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:589
Contre-vérification — Redérivé : durations() (RentalContract.java:1727-1743) itère sur les mois entre periodFirstDay/periodLastDay ; proratedMonth() (:1720) AFFECTE ratio[0] au lieu de l'accumuler, si bien qu'avec deux mois partiels seul le dernier subsiste, tandis que fullMonths() (:1703) n'en compte aucun. Atteignable : la première période CFR débute à getBegin() en cours de mois (:946-952), createRentPostingRule transmet getEnd() (:588-589) que le congé positionne à n'importe quel jour (ContractStateServiceImpl.java:369-371) ; les périodes trimestrielles/semestrielles/annuelles (:1787-1856) s'étendent sur plusieurs mois. Exemple début 14 nov./fin 20 déc. en trimestriel : facturé 20/31 et non 17/30+20/31. Les tests (RentalContractPeriodTest:574,651-841) ne combinent jamais début ET fin en cours de mois dans une même période. Gravité abaissée : conjonction rare (fréquence non mensuelle + début et fin en cours de mois dans la même période), perte bornée à un mois partiel par contrat.
Problème valide, déjà identifié à la revue du quittancement. Le ratio est écrasé au lieu d'être cumulé : le premier mois partiel est perdu quand une période contient deux mois partiels. Le calcul devrait cumuler les ratios de mois partiels.
Selon le chemin de calcul emprunté, un même montant se terminant par un demi-centime est arrondi tantôt vers le haut, tantôt vers le bas : 566,665 € devient 566,67 € par un chemin et 566,66 € par un autre. Comme l'arrondi s'applique à chaque étape intermédiaire des calculs en chaîne, de petits écarts s'accumulent, et les honoraires et montants de TVA tombant exactement sur un demi-centime sont systématiquement arrondis vers le bas — à l'inverse de la convention commerciale et fiscale française (arrondi au centime le plus proche, demi-centime vers le haut). Il s'agit d'écarts au centime, mais répétés et incohérents, qui compliquent les rapprochements et les contrôles.
Money arrondit chaque construction à 2 décimales en HALF_DOWN — incohérent avec le HALF_UP utilisé partout ailleurs
Le constructeur de Money force l'arrondi de TOUS les montants en RoundingMode.HALF_DOWN, alors que Money.divide, Quantity.multiply/withScale, CallForRentUtil et LedgerAuxiliaryAccountServiceImpl utilisent tous HALF_UP. Une même valeur au demi-centime s'arrondit différemment selon le type qui la manipule en dernier : Quantity 566,665 → 566,67 (HALF_UP) mais Money.euros(566,665) → 566,66. Comme l'arrondi a lieu à l'intérieur de chaque add/subtract/multiply intermédiaire, l'arithmétique Money chaînée accumule des arrondis à chaque étape, et les montants d'honoraires/de TVA calculés à un demi-centime exact sont systématiquement arrondis vers le bas, divergeant de la convention commerciale/fiscale française standard (arrondi au centime le plus proche, demi-centime vers le haut) et du reste du code.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/Money.java:97adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/Quantity.java:162adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:104
Contre-vérification — Money.java:39,96-98 arrondit chaque Money construit (donc chaque résultat de add/subtract/multiply) avec setScale(2, HALF_DOWN), tandis que Money.divide (ligne 83), Quantity.withScale/multiply (Quantity.java:114,162), CallForRentUtil (:40,94) et LedgerAuxiliaryAccountServiceImpl:544 utilisent HALF_UP ; RentalFeesAmount:104 utilise HALF_DOWN. Divergence redérivée : Money.euros(566,665)=566,66 contre Quantity HALF_UP=566,67 ; des demi-centimes apparaissent sur la TVA à 10 % et les honoraires en pourcentage, et le chemin est actif dans AbstractFeesInvoiceCreationStrategy.java:126-127. Aucun test ni doc n'entérine HALF_DOWN comme intentionnel. Réserve : la règle d'« arrondi fiscal » citée s'applique légalement au niveau de l'euro (CGI 1724), c'est donc l'incohérence, non l'illégalité, qui constitue le défaut. Gravité moyenne maintenue.
HALF_DOWN est la règle d'arrondi correcte et convenue pour les montants comptables ; le HALF_UP de CallForRentUtil s'applique aux calculs de pourcentages, pas aux montants comptables finaux. La présence de modes d'arrondi différents n'indique donc pas en soi un bug — chaque usage doit être évalué selon le type de calcul, plutôt que d'imposer un mode global unique.
La TVA sur les honoraires et les montants calculés par pourcentage (par exemple la prime GLI) sont calculés avec une arithmétique informatique imprécise au lieu d'une arithmétique décimale exacte. Aux valeurs pile sur un demi-centime, cette imprécision peut faire basculer l'arrondi du mauvais côté et produire des écarts d'un centime sur les lignes de TVA des factures comptabilisées. L'égalité HT + TVA = TTC n'est alors plus garantie par construction : elle dépend du hasard des calculs, ce qui expose à des factures dont les totaux ne se recoupent pas au centime près.
Montants de TVA et de pourcentage calculés via arithmétique double (TaxCalculator, PercentageHelper)
TaxCalculator convertit les montants BigDecimal en double, calcule la TVA comme `(amount*rate)/(100+rate)` ou `amount + amount*rate/100` en virgule flottante binaire, puis ré-enveloppe via Money.euros(double). Aux limites exactes du demi-centime, l'erreur de représentation du double (p. ex. 100.11*20/120 = 16.684999999999999 au lieu de 16.685) inverse la décision d'arrondi, produisant des écarts d'1 centime par rapport à l'arithmétique décimale sur les lignes de taxe des factures comptabilisées (écritures 445xxx / 622xxx honoraires). PercentageHelper fait de même pour les montants basés sur un pourcentage (prime GLI via InsuranceRate). vatRate() reconstitue en outre le taux par division en double. La conservation des montants (HT + TVA == TTC) dépend alors de la chance du double plutôt que d'une identité arithmétique.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/TaxCalculator.java:29adb/adb-model/src/main/java/fr/lifeconnect/adb/model/utils/PercentageHelper.java:40
Contre-vérification — Redérivé : TaxCalculator.java:29-30 calcule la TVA en double ; Money.java:97 arrondit HALF_DOWN via BigDecimal.valueOf(double). Simulation sur 0,01–30000 € à 20 % : 81 505 montants au demi-centime pile basculent +1 centime par rapport au décimal exact (p. ex. 0,27 € TTC → double 0.045000000000000005 → 0,05 contre 0,04). PercentageHelper.java:40-43 même défaut. Appelants actifs : InsurancePaybackInvoiceCreationStrategy.java:119 et InsuranceContractInvoiceCreationStrategy.java:202-204 (factures GLI, 616500) ; TaxCalculatorTest ne couvre que des nombres ronds. Réserves : l'exemple 100.11 cité ne bascule pas (HALF_DOWN donne 16,68 dans les deux cas), et le périmètre d'impact se limite à l'assurance GLI — pas aux honoraires 622xxx. Gravité moyenne confirmée.
Choix d'implémentation intentionnel, pour garder des résultats de calcul cohérents ; le résultat est ensuite reconverti en Money et reçoit l'arrondi monétaire final. Sans cas concret produisant un centime erroné, à traiter comme une amélioration de précision possible, pas comme un bug fonctionnel confirmé.
Dans le service qui tient le grand livre, les montants sont enregistrés en base de données comme du texte et non comme des nombres, alors que le service des contrats enregistre les mêmes montants sous forme numérique. Conséquence : chaque calcul de solde ou de montant réglé/non réglé doit d'abord reconvertir le texte en nombre approximatif, ce qui fait passer des données comptables exactes par une représentation imprécise. Pire, toute recherche qui compare directement un montant sans cette conversion ne trouve silencieusement rien. C'est une fragilité structurelle du cœur comptable : pas d'erreur visible, mais des requêtes qui peuvent renvoyer des résultats faux ou vides.
adb-accounting n'a aucun convertisseur Mongo BigDecimal/Decimal128 — les montants du grand livre persistés en chaînes, toutes les requêtes passant par $toDouble
adb-contracts enregistre un WritingConverter Money→Decimal128, mais adb-accounting (le grand livre de référence) n'enregistre que des convertisseurs de dates, si bien que le mapping par défaut de Spring Data convertit chaque BigDecimal Money.amount embarqué en String Mongo. Par conséquent, aucun index/comparaison numérique ne fonctionne sur les montants stockés : chaque agrégation doit envelopper les montants dans $toDouble (tous les pipelines de solde, lettré/non lettré et montant ouvert), convertissant des données de grand livre en décimal exact via des doubles binaires au moment de la requête, et tout critère numérique direct sur un champ de montant (sans $toDouble) ne correspond silencieusement à rien car MongoDB ne fait jamais correspondre un nombre à une chaîne. Les deux services persistent en outre le même type Money dans deux représentations BSON incompatibles.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/config/MongoConfig.java:40adb-contracts/src/main/java/fr/lifeconnect/adb/contract/config/MongoConfig.java:67adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/InvoiceSearchRepositoryImpl.java:347
Contre-vérification — adb-accounting MongoConfig.java:40-49 n'enregistre que des convertisseurs de dates ; aucun convertisseur Money/BigDecimal n'existe dans le service ni dans adb-common. Boot 3.1.3 (adb/pom.xml:8) → Spring Data Mongo 4.1 mappe par défaut BigDecimal→String. Corroboré dans le code : CriteriaHelper.java:217 fait un match regex sur amount.amount (ne fonctionne que sur des chaînes) ; $toDouble enveloppe chaque pipeline de solde/lettrage (LedgerAccountRepository.java:104-111, DomainObjectEntryReactiveRepository.java:114-115/180-183, InvoiceSearchRepositoryImpl.java:299/335/347, BalanceAggregationOperations.java:96/107). adb-contracts MongoConfig.java:67 écrit du Decimal128, donc le type Money partagé a deux formes BSON. Le scénario « le critère numérique ne correspond à rien » n'est que latent — aucun cas fautif actif trouvé — donc gravité moyenne maintenue.
Intentionnel. L'absence de convertisseur Decimal128 dans adb-accounting n'est pas un oubli de configuration, et le $toDouble des agrégations découle du format de persistance choisi.
L'utilitaire censé répartir un montant en parts égales en redistribuant les centimes restants ne fait pas ce qu'il promet : les centimes résiduels sont perdus (10,00 € divisé en 3 donne trois parts de 3,33 € et 1 centime disparaît), et dans certains cas le mécanisme de correction ajouterait 1 € entier au lieu d'1 centime. Aujourd'hui, le seul calcul qui l'utilise (le loyer mensuel) n'est pas affecté en pratique, donc aucun montant réel n'est faux. Mais c'est un piège pour tout futur développement de répartition (charges, quotes-parts) qui s'appuierait sur cette garantie inexistante.
La redistribution des restes par plus fort reste de Quantity.divide(int) est cassée — centimes résiduels perdus, euros entiers ajoutés quand elle se déclenche
Le découpage en tableau Quantity.divide(denominator, scale) calcule `remainder = amount.subtract(simpleResult.multiply(bigDenominator)).intValue()` : le résidu après arrondi par part est fractionnaire (<1) donc intValue() le tronque à 0 et les centimes perdus/en trop ne sont jamais redistribués (10,00/3 → trois parts de 3,33, 0,01 perdu ; somme des parts != total). Si le résidu atteignait jamais une unité entière, la boucle ajouterait 1,00 EUR (et non 0,01) aux premières parts. Le seul appelant actif, RentalContract.calculateMonthlyRent (:846, scale 4, prend [0]), maintient le résidu sous le centime, donc le défaut est actuellement latent, mais l'utilitaire annonce une garantie de conservation qu'il ne fournit pas et constitue un piège pour tout futur usage de découpage en parts.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/Quantity.java:184adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:846
Contre-vérification — Redérivé de Quantity.java:174-191 : le résidu après arrondi HALF_UP par part est borné par n*0,5*10^-scale, donc intValue() le tronque à 0 (10,00/3 au scale 2 perd 0,01 ; le cas de résidu négatif dans les propres données de QuantityTest.java:46-48, 152,45/3, sur-somme à 152,4501 et n'est jamais corrigé). Si remainder atteignait jamais 1, la boucle ajoute BigDecimal.valueOf(1) — une unité entière, pas un centime. Le seul appelant en tableau est RentalContract.java:846 (scale 4, prend [0]), où remainder vaut prouvablement toujours 0, donc le bug est latent comme affirmé. Gravité faible maintenue.
Problème valide. Le résidu est fractionnaire, mais intValue() tronque à zéro les valeurs inférieures à un : des centimes restants peuvent être perdus. La redistribution devrait opérer dans la plus petite unité de l'échelle demandée et répartir explicitement les unités résiduelles.
La plateforme accepte, à juste titre, un bail sans dépôt de garantie (obligatoire notamment pour un bail mobilité). Mais au moment de facturer la première période, le calcul de l'appel de dépôt plante sur ce montant manquant, et cette erreur contamine tout le premier appel : loyer et charges compris. Résultat : un bail sans dépôt ne peut jamais facturer sa première période — le locataire n'est pas appelé, le propriétaire n'est pas crédité, et il faut une intervention manuelle. Curieusement, un dépôt saisi à 0 € fonctionne, alors qu'un dépôt laissé vide bloque tout.
Un bail avec rentDepositAmount null provoque une NPE dans la règle de passation CALL_FOR_DEPOSIT et met en erreur tout le premier appel de loyer
RentalContract.createPostingRule construit la règle CFD avec `Money.euros(rentDepositAmount.getAmount())` — NPE lorsque rentDepositAmount est null (et le constructeur de Money provoque une NPE sur un BigDecimal null). Le validateur prêt-à-signer autorise explicitement montant=null + bénéficiaire=null (un bail sans dépôt légitime, obligatoire pour le bail mobilité). La NPE est capturée par le onErrorResume générique de preProcessEvent, l'enfant CFD passe en ERROR, puis withFriendsErrorMessages/propagateErrorToParent bascule le parent INITIAL_RENT et tous les frères CALL_FOR_RENT / CALL_FOR_RENTAL_CHARGES en ERROR : un bail sans dépôt ne peut jamais facturer sa première période. Asymétrie : montant=0 lève ZeroAmountException qui est ignorée silencieusement (Mono.empty), donc zéro fonctionne mais null bloque la facturation.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:566adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/AccountingEventServiceImpl.java:468adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.java:109adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/Money.java:96
Contre-vérification — Entièrement redérivé. RentalContract.java:566-567 provoque une NPE sur rentDepositAmount null pour CALL_FOR_DEPOSIT ; RentalContractReadyToSignValidator.java:109-124 autorise explicitement montant null + bénéficiaire null, donc les baux sans dépôt se signent. InitialRent.friends() (InitialRent.java:131-140) ajoute CallForDeposit sans condition ; aucun garde-fou dans les handlers de pré-chaîne. DefaultHandlerChain enveloppe les handlers dans Mono.defer, donc la NPE atteint le onErrorResume générique (AccountingEventServiceImpl.java:472) → enfant ERROR → propagateErrorToParent/propagateErrorToOtherChildren (:359-400) bloque INITIAL_RENT et tous les frères. Le chemin montant-zéro (ZeroAmountException, :468) est ignoré silencieusement — asymétrie confirmée. La référence légale est légèrement erronée (art. 25-13, pas 25-12) mais sans incidence.
Problème valide. La validation frontend l'empêche normalement, mais le modèle backend peut encore contenir un rentDepositAmount null. CALL_FOR_DEPOSIT est créé pendant INITIAL_RENT (première salve comptable) ; si le montant du dépôt manque, la création de la règle de comptabilisation lève une exception et met tout le lot CFR initial en erreur. Le backend devrait soit ignorer CALL_FOR_DEPOSIT quand aucun dépôt n'est configuré, soit rejeter le contrat avant le démarrage de la comptabilité.
Lors de la signature d'un bail, la plateforme vérifie seulement que le montant du dépôt de garantie et son bénéficiaire sont renseignés ensemble, jamais que le montant respecte le plafond légal (1 mois de loyer hors charges en location vide, 2 mois en meublé). Un gestionnaire peut donc signer un bail avec un dépôt de 3 mois : le système l'appelle et le comptabilise sans broncher. Par ailleurs, le bail mobilité, pour lequel tout dépôt de garantie est interdit, n'existe pas comme type de contrat, donc cette interdiction ne peut même pas être appliquée. C'est une exposition juridique directe : les locataires peuvent contester et exiger le remboursement du trop-perçu.
Aucune validation du plafond légal sur le montant du dépôt de garantie ; le bail mobilité n'est pas du tout modélisé
Les seuls contrôles sur le dépôt de garantie au stade prêt-à-signer portent sur la présence croisée du montant et du bénéficiaire ; rentDepositAmount n'est jamais comparé au loyer. Un gestionnaire peut créer et signer un bail d'habitation vide avec un dépôt de 3 mois (maximum légal : 1 mois de loyer hors charges, loi 89-462 art. 22) ou un bail meublé au-delà de 2 mois (art. 25-6) ; la plateforme appellera et comptabilisera ce montant sans broncher. Les catégories de contrat se limitent à CIVIL/COMMERCIAL/FURNISHED_RESIDENTIAL/EMPTY_RESIDENTIAL — le bail mobilité (où tout dépôt de garantie est interdit) n'existe pas comme type de bail, si bien que l'interdiction ne peut même pas être exprimée. Une recherche des mots-clés de plafond (plafond, un mois, comparaison deposit*rent) dans la validation d'adb-contracts ne renvoie rien.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.java:109adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/RentalContractCategory.java:7
Contre-vérification — RentalContractReadyToSignValidator.java:109-123 se contente de vérifier de façon croisée la présence du montant et du bénéficiaire du dépôt ; aucun plafond dépôt-vs-loyer nulle part dans la validation d'adb-contracts (le grep le confirme). RentalContractCategory.java:8 ne comporte aucun type mobilité ; zéro occurrence de 'mobilit/MOBILITY' dans adb-contracts, adb-model, adb-ui, adb-web, adb-doc. Les règles légales sont correctement énoncées (loi 89-462 art. 22, 25-6, 25-13). La gravité est toutefois surévaluée : adb-ui rc-conditions.component.ts:678-688 pré-remplit le dépôt légalement correct (2x meublé, 1x vide) ; l'infraction suppose une surcharge manuelle délibérée via un champ éditable non validé, et le risque pèse côté bailleur, non sur l'intégrité comptable. À rétrograder en medium.
Ces validations ne faisaient pas partie des exigences convenues. Le gestionnaire saisit le montant du dépôt manuellement à la création du contrat et aucun plafond backend n'a été spécifié. À traiter comme une lacune de validation légale/métier potentielle, pas comme une régression — tout nouveau plafond doit d'abord être confirmé côté métier ou juridique.
La seule règle existante sur la restitution du dépôt de garantie exige simplement qu'un délai en mois soit renseigné, et uniquement pour les reprises de contrats civils. Rien ne bloque un délai illégal (6 mois passe sans alerte), la distinction entre le délai d'1 mois (état des lieux conforme) et de 2 mois n'est pas gérée, aucun suivi ni relance ne surveille la date limite de restitution après la fin du bail, et la pénalité légale due au locataire en cas de retard (10 % du loyer mensuel par mois de retard commencé) n'est calculée nulle part. En pratique, le respect de la loi repose entièrement sur la vigilance manuelle des gestionnaires, avec un risque de condamnation à des pénalités en cas d'oubli.
Les délais légaux de restitution et la pénalité de restitution tardive sont totalement absents ; des délais de restitution illégaux sont acceptés
La seule logique de restitution de tout le monorepo est RentDepositRestitutionPeriodRule, qui exige uniquement que le champ existe avec l'unité MONTH — et seulement pour les contrats civils TAKE_OVER (elle n'est enregistrée que dans CivilRentalRentalContractTakeOverValidator). Rien ne plafonne la valeur aux 2 mois légaux (un délai de 6 mois est validé), la distinction 1 mois (EDL conforme) vs 2 mois n'est pas modélisée (aucun indicateur de conformité de l'EDL), aucun ordonnanceur/relance ne suit l'échéance de restitution après la fin du bail, et la pénalité de retard de 10 % du loyer mensuel par mois entamé due au locataire n'est calculée nulle part (grep penalt/majoration/restitution sur les deux services : seule cette règle). La retenue de 20 % jusqu'à la régularisation annuelle de copropriété n'a pas non plus de modèle — la retenue n'est possible que via des écritures d'ajustement manuelles en saisie libre.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/rules/RentDepositRestitutionPeriodRule.java:18adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/validators/CivilRentalRentalContractTakeOverValidator.java:22
Contre-vérification — Redérivé : RentDepositRestitutionPeriodRule.java:18-38 vérifie seulement le caractère non nul et unit=="QUANTITY.MONTH" ; l'unique enregistrement est CivilRentalRentalContractTakeOverValidator.java:22 (absent de NewRentalRentalContractValidator et RentalContractReadyToSignValidator). Aucun plafond nulle part — adb-ui rc-conditions.component.html:632 propose même 1-12 mois, donc 6 mois se valide de bout en bout. Le grep penalt/majoration ne trouve qu'un champ de modèle inutilisé (RentalContract.java:378 penaltySurcharge, réservé aux gabarits de document) ; aucun indicateur de conformité EDL, aucun ordonnanceur (uniquement ContractRenewalJobExecutor), la relance ne couvre que les impayés de loyer. Le remboursement du dépôt n'existe qu'en tant qu'événement manuel TENANT_DEPOSIT_REIMBURSEMENT (doc : Manuel=oui). La règle légale (loi 89-462 art. 22) est exactement énoncée. La gravité medium est appropriée.
La règle actuelle vérifie seulement que, pour un TAKE_OVER civil, le délai de restitution existe et utilise MONTH comme unité ; elle ne valide pas le nombre de mois ni ne calcule de pénalité de retard. Ma compréhension : le gestionnaire était responsable de saisir le délai correct, le système ne validant que le format des données. Ce flux est antérieur à mon arrivée et je ne me souviens pas d'exigences de validation métier supplémentaires. Statut approprié : nécessite une clarification métier/juridique, plutôt qu'un bug confirmé.
Le remboursement du dépôt de garantie au locataire est enregistré à partir d'un montant saisi librement, sans aucune vérification. Le système ne contrôle pas que le dépôt est bien détenu par l'agence (le rembourser sur un bail où c'est le propriétaire qui le détient crée une dette de dépôt négative dans les comptes), ni que le dépôt a réellement été encaissé ou transféré (un remboursement anticipé laisse les comptes de dépôt définitivement déséquilibrés), ni que le montant remboursé ne dépasse pas le dépôt du contrat : un sur-remboursement passe sans alerte. C'est un risque direct de sortie d'argent injustifiée et de comptes de dépôts faux, détectables seulement par un pointage manuel.
TENANT_DEPOSIT_REIMBURSEMENT est une écriture manuelle non protégée : ni détenteur, ni solde, ni validation de montant
Le remboursement passe toujours l'écriture 165200 ↔ 411100/RC à partir d'un montant et d'un sens fournis par l'utilisateur. Le backend ne vérifie jamais (a) que rentDepositBeneficiary=MANDATE (l'événement est documenté comme réservé au DG détenu par l'agence — le passer sur un contrat où le dépôt est détenu par le bailleur débite un 165200 jamais crédité, créant une dette de dépôt négative), (b) que le dépôt a effectivement été encaissé/transféré (avant lettrage le dépôt figure au 165100 ; rembourser laisse alors le 165100 ouvert en permanence pendant que le 165200 devient négatif), ni (c) que le montant est ≤ rentDepositAmount du contrat / au solde courant du 165200 — le sur-remboursement est accepté silencieusement. La question ouverte 9 du doc takeover-ui-rules (validation backend vs UI seule) est ainsi tranchée : rien n'est validé côté backend pour les événements manuels de dépôt.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/RentPostingRuleFactory.java:96adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1179adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForDeposit.java:88
Contre-vérification — Redérivé depuis le code. RentPostingRuleFactory.java:96-112 transmet directement le montant/sens utilisateur à manualEntry (165200 vs 411100) ; RentalContract.java:1179 dispatche sans aucun contrôle de bénéficiaire/solde. La validation backend est seulement au niveau bean : ManualEntryEvent.java:220-246 vérifie sens/eventNature/période/postingDate, AccountingEventController.java:52-83 applique uniquement @Validated ; aucun garde-fou spécifique à la nature dans adb-accounting ni adb-contracts (le grep sur rentDepositBeneficiary/165200 n'en trouve aucun). PostingRuleFactory.java:258-268 confirme que 165100→165200 n'a lieu que via TenantToAgency, émis uniquement quand beneficiary=MANDATE (CallForDeposit.java:99-108), donc un remboursement détenu par le bailleur ou antérieur au lettrage rend le 165200 négatif ; le sur-remboursement n'est pas contrôlé. L'UI (entry-details.component.ts:84) propose la nature sans condition. Nuance mineure : takeover-ui-rules.md liste une question ouverte, pas une "question 9". La gravité medium est juste : déclenché par l'opérateur, réversible, mais lacune réelle d'intégrité grand livre/FEC.
Intentionnel. Une écriture manuelle existe pour permettre à l'utilisateur de créer les enregistrements comptables voulus, avec le montant et le sens choisis ; elle n'est pas censée être dérivée automatiquement ni contrainte par le détenteur du dépôt ou un solde calculé. La correction de l'opération manuelle relève de l'utilisateur qui la crée.
Les honoraires en pourcentage déclenchés par l'appel de loyer prennent pour base tout ce qui est appelé au locataire sur la période, y compris le dépôt de garantie. Sur la première période d'un nouveau bail, les honoraires de gestion sont donc calculés sur loyer + charges + dépôt complet. Avec un dépôt d'un à deux mois de loyer, le propriétaire est surfacturé du pourcentage de gestion sur un à deux mois supplémentaires à chaque nouvelle location — alors que la pratique standard exclut le dépôt de garantie de l'assiette des honoraires. Ne concerne que la première période de chaque bail, mais de façon systématique.
Les honoraires en pourcentage déclenchés par l'appel de loyer intègrent le dépôt de garantie dans leur assiette
L'assiette de la facture d'honoraires pour les honoraires déclenchés par le CFR additionne toutes les écritures du compte locataire de types CALL_FOR_RENT, CALL_FOR_RENTAL_CHARGES, CFR_CHARGES_HOUSEHOLD_WASTE et CALL_FOR_DEPOSIT pour la période (débits moins crédits sur la cible RC — la jambe crédit 165100 n'ayant pas de cible RC, elle ne se compense pas). Tout honoraire en pourcentage configuré avec le déclencheur CFR (typiquement des honoraires de gestion, 622640) est donc calculé sur loyer + charges + la totalité du dépôt lors de la période initiale. Avec un dépôt de 1 à 2 mois de loyer, le propriétaire est surfacturé du pourcentage de gestion de 1 à 2 mois supplémentaires à chaque nouveau bail. La pratique standard calcule les honoraires de gestion sur le quittancement/les encaissements en excluant le DG, qui n'est pas un revenu du propriétaire.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/CallForRentFeesInvoiceCreationStrategy.java:31adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/AbstractFeesInvoiceCreationStrategy.java:117adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:63
Contre-vérification — Redérivé : CALL_FOR_DEPOSIT figure dans FEE_BASIS_EVENTS_CFR_AWARE (CallForRentFeesInvoiceCreationStrategy.java:31-36) ; sa seule jambe ciblée RC est le débit 411100 (RentPostingRuleFactory.java:508-532), donc amount() (lignes 97-108) ajoute la totalité du dépôt ; descriptor.callForRentPeriod correspond à la période CFR initiale (InitialRent.java:132-141, DescriptorHandler.java:207-215). L'honoraire en pourcentage s'applique à cette assiette (AbstractFeesInvoiceCreationStrategy.java:126, RentalFeesAmount.java:96-107). adb-ui configure par défaut les honoraires de gestion en PERCENTAGE avec déclencheur CFR (contract.mapper.ts:807), et SettlementFeesInvoiceCreationStrategy.java:34 exclut délibérément les dépôts — confirmant l'inclusion non intentionnelle.
Logique métier attendue. Selon la spécification, la base de calcul des honoraires inclut intentionnellement CALL_FOR_DEPOSIT — ce constat n'est donc pas un bug.
Le contrôle qui décide si un contrat a encore une échéance à appeler raisonne comme si tous les baux étaient mensuels. Sur un bail trimestriel (ou semestriel/annuel) payé à terme échu qui se termine au 1er ou 2e mois de son dernier trimestre — par exemple une fin de bail au 15 avril — le système conclut à tort qu'il n'y a plus rien à facturer. Le dernier loyer prorata (du 1er au 15 avril dans l'exemple) n'est jamais appelé au locataire, et le contrat ne peut jamais être clôturé proprement dans le système. C'est à la fois du loyer non perçu et un dossier qui reste bloqué. Un cas voisin exclut aussi un bail se terminant exactement le premier jour d'une période.
Dernière période au prorata jamais facturable pour les contrats trimestriels/semestriels/annuels payés à terme échu
isPeriodValidForContract calcule le début de la période couverte à terme échu via period.atDay(1).minusMonths(1) — en codant en dur une fréquence MONTHLY. Pour un contrat trimestriel à terme échu, l'étiquette de la dernière période est le mois suivant la fin du trimestre (par ex. 2023-07 pour avril–juin) ; le contrôle calcule le 1er juin et le compare à la fin du contrat. Si le bail se termine au mois 1 ou 2 de son dernier trimestre (par ex. fin le 15 avril), le 1er juin n'est pas antérieur au 15 avril, le contrat est écarté des candidats, et le dernier trimestre au prorata (1er–15 avril) ne peut jamais être appelé : le locataire n'est jamais facturé du dernier loyer et la transition FINAL/ENDED n'a jamais lieu. De plus, le `before()` strict exclut un bail se terminant exactement le premier jour d'une période (1 jour occupé jamais facturé).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:158adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractAccountingServiceImpl.java:513
Contre-vérification — Re-dérivé : étiquette d'appel de loyer à terme échu = mois suivant fullPeriodLastDay, qui ignore la fin du contrat (RentalContract.java:933-944, QuarterlyPeriod:1798). ContractHelper.getPeriodFirstDay:162 soustrait exactement 1 mois (mensuel codé en dur ; le test ContractHelperTest.java:60-87 ne couvre que le mensuel), donc un bail trimestriel à terme échu se terminant le 15 avril (étiquette 2023-07) échoue à periodFirstDate.before(end) dans ContractHelper.java:179 et est écarté de tous les chemins d'appel de loyer (ContractAccountingServiceImpl.java:506-525 verrouille chaque point d'entrée, :136-164). nextCallForRentPeriod n'avance jamais ; aucun chemin de facturation alternatif n'existe (reCalculateNextCallForRentPeriod est réservé à la reprise de gestion, même étiquette). Le cas limite du before strict est également réel. Quarterly+terme échu est créable via l'UI de prod (contract.catalog.service.ts:13, rc-conditions.component.ts:693). Sévérité haute maintenue.
Problème valide. La validation des candidats retranche exactement un mois pour tout contrat à terme échu, au lieu d'utiliser la fréquence de paiement réelle. Difficile à repérer : il faut la combinaison date de fin explicite + IN_ARREARS + fréquence non mensuelle ; le plus souvent, begin+1 mois tombe quand même avant la date de fin. Cette logique ne sert qu'à filtrer les candidats CFR (elle n'affecte pas le calcul du loyer) mais devrait utiliser le vrai rentPaymentBasisPeriod.
Pour les baux facturés au trimestre, au semestre ou à l'année, si une révision de loyer (indexation IRL par exemple) prend effet au 2e ou 3e mois de la période, le quittancement n'en tient pas compte : toute la période est appelée à l'ancien montant. La différence n'est jamais rattrapée ensuite — la période suivante démarre simplement au nouveau tarif — et le système marque pourtant le nouveau montant comme déjà facturé. Résultat : du loyer indexé définitivement non perçu pour le propriétaire, sans aucune alerte. Les baux payés au mois ne sont pas concernés.
Changement de loyer en cours de période (indexation) silencieusement ignoré pour un quittancement trimestriel/semestriel/annuel
createRentPostingRule construit la période du contrat avec rentBasisPeriod (typiquement MONTHLY selon la fixture supportée : rentBasisPeriod=MONTHLY + rentPaymentBasisPeriod=QUARTERLY), si bien que la période utilisée pour rechercher un montant de loyer futur ne couvre que le PREMIER mois de la période de quittancement (MonthlyPeriod.fullPeriodLastDay = fin du mois de début). findFutureAmount est bornée par cette période, donc un montant indexé prenant effet au mois 2 ou 3 d'un trimestre n'est jamais trouvé : tout le trimestre est facturé à l'ancien tarif et l'écart n'est jamais rattrapé (le trimestre suivant démarre simplement au nouveau tarif). Pire, lockAlreadyUsedAmounts(lastDayOfPeriod) verrouille ensuite le montant jamais facturé. Exemple : loyer mensuel de 1000, payé trimestriellement, indexation à 1030 prenant effet le 14 novembre → 14 nov.–31 déc. facturé à 1000 ; environ 45 jours d'écart d'indexation perdus chaque année pour tout contrat dont la date anniversaire ne tombe pas dans le premier mois d'un trimestre de quittancement.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:588adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentAmount.java:113adb/adb-model/src/test/java/fr/lifeconnect/adb/model/contract/RentalContractPeriodTest.java:1026
Contre-vérification — Re-dérivé du code : RentalContract.java:588 utilise rentBasisPeriod, donc avec une base MONTHLY + un paiement QUARTERLY la fenêtre de findFutureAmount (RentAmount.java:113, MonthlyPeriod fullPeriodLastDay RentalContract.java:1773) ne couvre que le mois 1 du trimestre ; les montants indexés en milieu de trimestre (whenEffective = date anniversaire, NextExpectedIndexDataCreator.java:14) sont ignorés, puis verrouillés via lastDayOfPeriod (RecurringRentAccountingEventProcessor.java:117,123). Aucun événement de rattrapage n'existe. Mitigation : l'UI de prod force rentPaymentBasisPeriod=rentBasisPeriod (rc-conditions.component.ts:693 ; contracts.service.ts:1315), et des champs égaux se comportent correctement — l'exposition se limite donc aux contrats via API/reprise de gestion/hérités correspondant à la fixture trimestrielle du modèle (RentalContractPeriodTest.java:1026). Sévérité abaissée de haute à moyenne.
Ne reflète pas le flux réel. À la création d'un CALL_FOR_RENT, le RentAmount applicable est sélectionné via sa date whenEffective ; si deux montants s'appliquent dans une même période de facturation, celle-ci est scindée à whenEffective et des écritures distinctes sont produites par sous-période (par ex. quatre écritures au lieu de deux). Le même principe vaut pour les autres valeurs de rentBasisPeriod : une indexation en milieu de période n'est donc pas silencieusement ignorée.
Le calcul du prorata ne sait retenir qu'un seul mois incomplet par période de facturation : s'il y en a deux, le premier est écrasé par le second. Cela arrive sur un bail facturé au trimestre (ou plus) qui commence et se termine en cours de mois — par exemple un bail court du 14 novembre au 15 décembre : le prorata de novembre (environ 567 EUR sur un loyer de 1 000 EUR) disparaît, seul celui de décembre est appelé. Le même phénomène peut se produire quand une indexation tombe en milieu de mois. C'est du loyer perdu en silence, que ni le locataire ni le gestionnaire ne voient signalé.
proratedMonth() écrase au lieu de sommer plusieurs mois partiels — le loyer d'un mois partiel silencieusement perdu
AbstractContractPeriod.durations() parcourt chaque mois de la période de quittancement ; proratedMonth() AFFECTE ratio[0] à chaque mois partiel au lieu de l'accumuler. Une période de quittancement trimestrielle/semestrielle/annuelle (ou une sous-période scindée) qui commence et se termine toutes deux en milieu de mois contient deux mois partiels, et seul le dernier est facturé. Exemple : un bail payé trimestriellement débutant le 14 novembre et se terminant le 15 décembre (bail court ou préavis) : les 17/30 de novembre (≈566,67 sur un loyer de 1000/mois) sont perdus, seuls les 15/31 de décembre sont facturés. La même perte survient pour une sous-période scindée lorsqu'une date d'indexation tombe en milieu de mois à l'intérieur d'une période à base trimestrielle démarrant en milieu de mois. fullMonths() accumule correctement, ce qui rend l'asymétrie clairement un bug.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1715
Contre-vérification — RentalContract.java:1720 affecte ratio[0] au lieu d'accumuler, alors que fullMonths() (:1708) accumule ; durations() (:1727) peut émettre deux mois partiels lorsqu'une période QUARTERLY/HALF_YEARLY/YEARLY est bornée en milieu de mois aux deux extrémités (periodFirstDay=début du contrat au premier appel, :946-969 ; periodLastDay=getEnd(), :1484, :588-590) ou scindée en milieu de mois (:807-809, :1695). Le loyer du premier mois partiel est silencieusement perdu ; toutes les règles d'écriture passent par periodAmounts (:868-869). Aucun validateur ne force des dates alignées sur les bornes de mois ; les tests ne couvrent que les cas à un seul mois partiel. Rétrogradé à moyen : seule la première période de quittancement des contrats non mensuels avec deux bornes en milieu de mois est exposée — perte rare et bornée.
Problème d'implémentation valide. durations() peut visiter plus d'un mois partiel, mais proratedMonth() affecte le ratio au lieu de le cumuler ; quand une période commence et se termine sur des mois partiels, le ratio du premier mois partiel peut être écrasé par celui du dernier. L'implémentation devrait cumuler les ratios partiels (la méthode est censée en retourner le total).
Quand un appel de loyer échoue côté comptabilité, la période concernée est marquée en erreur, mais rien ne permet ensuite de la relancer : le quittancement ne considère que les périodes neuves. Le même blocage existe si une période reste coincée « en cours de traitement » parce que le déclenchement automatique s'est perdu. Dans les deux cas, le bail devient définitivement infacturable : plus aucun appel de loyer, plus de quittances, jusqu'à une intervention manuelle d'un technicien directement dans la base de données. À noter que les prélèvements automatiques, eux, savent réessayer — cette sécurité manque uniquement au quittancement.
Aucune transition de récupération pour les périodes d'appel de loyer bloquées en ERROR (ou PROCESSING) — le contrat devient définitivement non facturable
L'endpoint de facturation ne sélectionne que les périodes de loyer à l'état INITIAL (query states List.of(INITIAL) et filtre de candidats INITIAL). Lorsque la comptabilité signale un événement en ERROR, RecurringRentAccountingEventProcessor.onErrorEventState positionne nextCallForRentPeriod.state=ERROR ; aucun code dans adb-contracts ne réinitialise jamais ERROR→INITIAL (contrairement aux prélèvements, qui rejouent `$in [INITIAL, ERROR]`). De même, après le CAS INITIAL→PROCESSING, l'appel comptable réel repose sur un trigger MongoDB Atlas (callAccounting est une méthode vide) ; si l'événement du trigger est perdu, la période reste PROCESSING indéfiniment sans chemin de reprise. Dans les deux cas, le contrat ne peut plus jamais être facturé sans intervention manuelle en base — une impasse de machine à états sur le chemin critique pour le chiffre d'affaires.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractAccountingServiceImpl.java:393adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/processor/accounting/impl/RecurringRentAccountingEventProcessor.java:255adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessAccountingEventStrategy.java:71
Contre-vérification — ERROR est une véritable impasse : la facturation ne sélectionne que INITIAL (ContractAccountingServiceImpl.java:393, :512) ; AccountingEventHelper.isCallForRentPeriodNew:180-184 exige l'état PROCESSING, donc même un événement PROCESSED ultérieur ne peut effacer ERROR ; le contractCallForRentCallBack d'adb-accounting ne régénère que si l'état est PROCESSING (ContractsFunctions.java:242-246). Aucun endpoint/UI ne le réinitialise ; seule une intervention manuelle mongosh existe (adb-infrastructure/mongodb-atlas/data-scripts/reset_accounting.js), corroborant la lacune. Réserve : le volet PROCESSING est surestimé — toute mise à jour ultérieure du contrat re-déclenche le trigger Atlas et la comptabilité régénère les événements pour les périodes PROCESSING, donc seul ERROR est irrécupérable. Sévérité moyenne maintenue (chemin du chiffre d'affaires, mais rare et récupérable côté ops).
Problème ouvert connu. Les mécanismes de reprise/relance des événements comptables en échec étaient prévus mais n'ont pas été terminés. Comme la facturation ne sélectionne que les périodes en INITIAL, une période affectée peut nécessiter une intervention manuelle ; le statut approprié est prévu mais pas implémenté.
Lorsque la comptabilité confirme un appel de loyer, le système met à jour le bail (période appelée, montants verrouillés). Si une erreur survient pendant cette mise à jour, elle est simplement journalisée et le bail est enregistré dans l'état intermédiaire où il se trouvait — parfois sans aucune trace de l'appel. Or les écritures comptables (débit locataire, produit locatif) sont déjà passées. Le bail et le grand livre se contredisent alors durablement : le loyer apparaît « non appelé » côté gestion alors que le locataire est bien débité, et le blocage décrit par ailleurs empêche toute reprise automatique. Cela fausse le suivi des impayés et les comptes rendus de gestion, et exige une correction manuelle.
Le processeur de retour d'appel de loyer avale les erreurs et persiste un contrat à moitié mis à jour alors que les écritures existent déjà
Dans RecurringRentAccountingEventProcessor.handle, toute erreur levée pendant l'enregistrement de la période appelée (periodCalled, transition d'état, verrouillage du montant) est capturée par onErrorResume, journalisée, et la chaîne CONTINUE vers persistCallForRentPeriods avec l'état dans lequel l'objet RentalContract muté sur place se trouve. Si l'erreur est survenue avant que periodCalled ne mute le contrat, la période est persistée toujours en PROCESSING sans calledPeriod enregistré, alors qu'adb-accounting a déjà passé les écritures 411100/706100 — le contrat et le grand livre divergent définitivement (le loyer apparaît « non appelé » alors que le locataire est débité), et l'impasse PROCESSING ci-dessus empêche toute reprise. Si l'erreur est survenue en cours de mutation, un état partiel (par ex. période avancée sans transition d'indexation) est persisté.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/processor/accounting/impl/RecurringRentAccountingEventProcessor.java:127
Contre-vérification — RecurringRentAccountingEventProcessor.java:127-133 avale toute erreur et continue vers persistCallForRentPeriods avec le contrat muté sur place. Les deux scénarios re-dérivés : (a) un échec transitoire de la récupération des écritures comptables (ProcessAccountingEventStrategy.java:93) laisse le contrat en PROCESSING sans calledPeriod alors que les écritures sont déjà passées ; le listener SQS acquitte inconditionnellement (AccountingFunction.java:60) et le swallow contourne le chemin DLQ (SqsMessageHandler.java:87), donc la reprise est silencieusement perdue ; la sélection des candidats d'appel de loyer ne retient que INITIAL (ContractAccountingServiceImpl.java:393), donc le contrat se bloque. (b) RentalContract.java:900-904 mute nextCallForRentPeriod avant calledPeriods.get(0), donc un throw en cours de mutation persiste une période avancée sans appel enregistré. Sévérité moyenne maintenue.
Comportement intentionnel. À ce stade, les données comptables critiques ont déjà été traitées ; la mise à jour du contrat est l'étape finale, essentiellement informative. Une erreur sur une mise à jour de contrat ne doit pas empêcher persistCallForRentPeriods de s'exécuter — poursuivre le pipeline est une stratégie de reprise « au mieux », pour persister le plus d'état possible. Ces échecs doivent néanmoins rester visibles via des logs et du monitoring appropriés.
Pour un bail payé à terme échu à une fréquence autre que mensuelle, le tout premier appel de loyer est rattaché à un mois qui ne suit pas la convention utilisée pour les appels suivants. Exemple : un bail trimestriel débutant le 14 novembre voit son premier appel (couvrant novembre-décembre) étiqueté « décembre » alors que les suivants sautent directement à avril — le libellé « janvier » n'existe jamais. Aucun loyer n'est perdu : les mois couverts restent continus. En revanche, les tableaux de suivi des périodes, les récapitulatifs PDF et certaines recherches par période affichent une clé de période trompeuse pour ce premier trimestre, ce qui peut dérouter les gestionnaires.
L'étiquette de la période d'appel initiale à terme échu code en dur début+1 mois, incohérente avec la convention à terme échu pour les fréquences non mensuelles
buildCalledPeriods étiquette le tout premier appel à terme échu comme mois-de-début+1, quelle que soit la fréquence de paiement, alors que calculateNextCallForRentPeriod étiquette les périodes à terme échu comme le mois de (fin de période + 1 jour). Pour un bail trimestriel à terme échu débutant le 14 novembre, l'appel initial passé le 1er janvier couvrant le 14 nov.–31 déc. est classé sous la période « 2022-12 » (un mois à l'intérieur de la période couverte) et la période suivante saute à 2023-04, si bien que l'étiquette 2023-01 n'existe jamais. Le test unitaire affirme cet étiquetage incohérent. Aucun montant n'est perdu (les mois couverts restent continus), mais les tableaux de bord des périodes de loyer, les PDF de récapitulatif et les recherches indexées par période (isPeriodValidForContract) opèrent sur une clé de période trompeuse pour le premier trimestre.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:982adb/adb-model/src/test/java/fr/lifeconnect/adb/model/contract/RentalContractPeriodTest.java:593
Contre-vérification — Re-dérivé : RentalContract.java:982-985 étiquette l'appel de loyer initial à terme échu comme début+1 mois, sans tenir compte de la fréquence, alors que calculateNextCallForRentPeriod (RentalContract.java:934-940) étiquette les périodes à terme échu comme le mois de (fullPeriodLastDay+1). RentalContractPeriodTest.java:576-604 (trimestriel à terme échu, début 2022-11-14) affirme l'incohérence : étiquette initiale « 2022-12 » malgré une date de passation au 2023-01-01, puis « 2023-04 »/« 2023-07 » qui ELLES correspondent aux mois de passation ; « 2023-01 » n'existe jamais. Le test mensuel à terme échu (~ligne 553) montre que la convention est étiquette=mois de passation. L'étiquette est consommée par le $group de RentPeriodRepository sur callForRentPeriods.period et par ContractHelper.getPeriodFirstDay (minusMonths(1)). Aucun impact monétaire ; sévérité basse correcte.
Intentionnel. Selon Pierre, la première période à terme échu suit cette convention begin+1 mois précisément parce qu'il s'agit de la période initiale ; l'écart entre le libellé initial et la convention des périodes suivantes est une règle métier explicite, pas un bug.
Le mois d'entrée dans les lieux, lorsque le dépôt de garantie est appelé avec le premier avis d'échéance, les honoraires de gestion au pourcentage sont calculés sur loyer + charges + dépôt de garantie, au lieu de loyer + charges seulement. Exemple : loyer 1 000 €, charges 100 €, dépôt 1 000 €, honoraires 8 % — la facture porte sur 2 100 € (168 € HT) au lieu de 1 100 € (88 € HT), soit près du double. Le dépôt de garantie n'est pas une somme quittancée et aucun mandat français ne prévoit d'honoraires de gestion dessus : chaque nouvel emménagement est donc surfacturé au propriétaire. De plus, l'assiette choisie dans le mandat (loyer seul, loyer + charges, tout encaissement) est purement ignorée par le calcul.
La base de calcul des honoraires de gestion inclut le dépôt de garantie (CALL_FOR_DEPOSIT)
La base utilisée pour les honoraires en pourcentage déclenchés sur le CFR additionne toutes les écritures du compte locataire pour les événements CALL_FOR_RENT, CALL_FOR_RENTAL_CHARGES, CFR_CHARGES_HOUSEHOLD_WASTE et CALL_FOR_DEPOSIT sur la période. Le mois d'entrée dans les lieux, lorsque le dépôt est appelé avec le premier CFR, les honoraires de gestion (%) sont calculés sur loyer + charges + dépôt de garantie. Exemple : loyer 1000, charges 100, dépôt 1000, honoraires de gestion 8 % => honoraires facturés sur 2100 (168 + TVA) au lieu de 1100 (88 + TVA) — près du double. Un dépôt de garantie n'est pas un montant encaissé/quittancé ; aucun mandat français ne prélève d'honoraires de gestion dessus. Le managementFeesBasis du RDC (RENT/RENTCHARGES/ANYRECEIPT) est par ailleurs totalement ignoré — le grep montre zéro lecture de ce champ dans adb-accounting.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/CallForRentFeesInvoiceCreationStrategy.java:31adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/CallForRentFeesInvoiceCreationStrategy.java:97adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.java:73
Contre-vérification — CALL_FOR_DEPOSIT figure dans FEE_BASIS_EVENTS_CFR_AWARE (CallForRentFeesInvoiceCreationStrategy.java:31-36) ; InitialRent génère un CallForDeposit sur la même période (InitialRent.java:132-141), dont le débit est passé au compte auxiliaire RC 411100 (RentPostingRuleFactory:511-521) avec descriptor.callForRentPeriod renseigné (DescriptorHandler.java:208-214), de sorte que amount() (lignes 97-107) gonfle la base des honoraires % du montant du dépôt (RentalFeesAmount.java:101-104). managementFeesBasis est validé à la signature (RentalDelegateContractReadyToSignValidator.java:114) mais jamais lu dans le calcul des honoraires. Rétrogradé en high : la facture est créée PARKED (validation humaine) et le surcoût n'intervient qu'une fois par bail.
Ne semble pas être un problème. Selon la spécification actuelle, les écritures listées, dont CALL_FOR_DEPOSIT, sont volontairement incluses dans la base de calcul des honoraires.
L'option consistant à facturer les honoraires au moment de l'encaissement du locataire (et non à l'appel de loyer) est proposée dans les écrans et acceptée dans les mandats, mais le mécanisme qui devait générer ces factures n'a jamais été mis en service : c'était un prototype volontairement débranché pour éviter de créer des données fausses. Une agence qui configure ses honoraires sur les sommes encaissées ne facture donc jamais rien, sans aucun message d'alerte. C'est un manque à gagner direct et silencieux tant que personne ne remarque l'absence de factures. Même remis en service tel quel, le mécanisme comporterait encore des erreurs sur certains encaissements.
La facturation d'honoraires sur base d'encaissement (honoraires sur encaissements) n'est reliée à rien — POC désactivé
Les honoraires dont le déclencheur est TENANT_PAYMENT_SETTLEMENT ne sont jamais facturés. Le callback qui les générerait (createInvoiceForSettlementCallback) est construit mais enregistré sur aucun listener SQS — onAccountingEntryModified ne relie que updateBalance et postSettlement — et porte le commentaire « Currently disabled, to avoid creating wrong data, was created as POC (V1) ». L'IHM propose toujours le déclencheur SETTLEMENT (rental-event-trigger-enum.ts:3) et le validateur ready-to-sign du RDC exige un managementFeesBasis ; ainsi, une agence configurant des honoraires sur les montants encaissés ne génère silencieusement aucune écriture d'honoraires, à jamais. Même réactivé, SettlementFeesInvoiceCreationStrategy émet un Mono.error sur tout lettrage ne soldant pas une écriture CALL_FOR_RENT (lettrages de charges/dépôt du même paiement), interrompant le batch.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/AccountingFunctions.java:74adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/SettlementFeesInvoiceCreationStrategy.java:34adb-ui/src/app/core/models/contract/rental-event-trigger-enum.ts:3
Contre-vérification — Assertion principale vérifiée : createInvoiceForSettlementCallback (AccountingFunctions.java:74, « Currently disabled...POC (V1) ») n'est enregistré sur aucun SqsListener (:124, :144-145) ; SettlementFeesInvoiceCreationStrategy.java:34,57-61 émettrait un Mono.error sur les lettrages non CALL_FOR_RENT. La doc (renting-management-owner-fees-without-vat.md) indique que les honoraires se déclenchent « sur CFR récurrent ou lettrage » ; le validateur RDC exige managementFeesBasis (RentalDelegateContractReadyToSignValidator.java:112-117) alors que la base n'est consommée nulle part dans adb-accounting. Correction : « aucune écriture d'honoraires à jamais » est faux — contract.mapper.ts:807 et TemplateValuesServiceImpl.java:339 codent en dur le déclencheur CFR, donc les honoraires sont tout de même facturés, mais sur base facturée et non encaissée. Sévérité abaissée en conséquence.
TENANT_PAYMENT_SETTLEMENT a été ajouté pour un usage futur. Le flux métier correspondant, calcul et facturation des honoraires sur lettrage, n'a pas encore été conçu avec l'équipe métier.
Le type « frais divers locataire (sans TVA) » est proposé dans les écrans de paramétrage, mais il n'a jamais été réellement câblé : le système le traite à tort comme un frais à la charge du propriétaire, ne sait pas dans quel compte le comptabiliser, et plante dès qu'il faut créer ou comptabiliser la facture. Pire, cette erreur est avalée en silence : toute la facture d'honoraires est abandonnée, y compris les autres honoraires valides du même contrat. Il suffit donc qu'un gestionnaire coche ce type de frais sur un bail pour que l'agence ne facture plus aucun honoraire sur ce contrat, sans aucune alerte.
RENTING_TENANT_MISC_FEES_WITHOUT_VAT est un chemin mort : mal classé en honoraire propriétaire, sans compte de charge, échoue à la construction/comptabilisation
Le type « honoraires divers locataire » existe dans l'enum, le catalogue et l'enum de l'IHM, mais : (1) RentalFeesAmount.isTenantFee() ne reconnaît que les honoraires locataire de dossier/état des lieux, de sorte qu'un honoraire divers locataire renvoie isOwnerFee()=true et atterrit dans la liste propriétaire (propriétaire en destinataire) ; (2) defineExpenseAccount n'a aucune branche pour lui et renvoie null ; (3) definePotentialEventType tombe dans la branche else et lève IllegalArgumentException. Configurer cet honoraire sur un contrat fait échouer la création de la facture d'honoraires ; dans ContractSignedFeesInvoiceCreationStrategy, l'erreur est absorbée par onErrorResume (lignes 78-81), supprimant silencieusement l'intégralité de la facture d'honoraires, y compris d'autres honoraires valides ; comptabiliser une facture dont l'item porte ce feeType échoue dans InvoicePostingRuleFactory.defineEventNature.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:75adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/InvoiceHelper.java:154adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/ContractSignedFeesInvoiceCreationStrategy.java:78adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/InvoicePostingRuleFactory.java:102
Contre-vérification — Toutes les assertions re-dérivées. RentalFeesAmount.java:75-78 omet RENTING_TENANT_MISC de isTenantFee(), il tombe donc dans ownerFees (AbstractFeesInvoiceCreationStrategy.filterFees). buildInvoiceItem appelle definePotentialEventType (ligne 135), qui lève une exception pour ce type (InvoiceHelper.java:170-172) ; ContractSignedFeesInvoiceCreationStrategy.java:78-81 absorbe l'erreur, supprimant toute la facture. defineExpenseAccount (InvoiceHelper.java:133-152) renvoie null. La comptabilisation échoue via InvoicePostingRuleFactory.java:102-105, et la liste déroulante de l'IHM expose le type pour les factures CONTRACT_SIGNED (outgoing-invoice-details-entry.component.ts:77-78). La doc events/renting-tenant-misc-fees-without-vat.md indique le statut Développé sans producteur. Rétrogradé en medium : l'IHM contrat (contract.mapper.ts:770-812) ne positionne jamais ce type d'honoraire, donc le chemin de suppression silencieuse nécessite des données uniquement via API ; l'échec accessible par l'IHM est une erreur bruyante de comptabilisation.
Ce constat ne reflète pas l'usage prévu de ce type d'honoraire. Il s'agit d'un honoraire personnalisé que l'utilisateur ajoute manuellement, et non d'un calcul automatique à partir d'écritures existantes. isTenantFee() sert à classer les honoraires calculés automatiquement en vue de leur traitement automatisé.
Les honoraires de location facturés au locataire (visite, constitution de dossier, rédaction de bail, état des lieux) sont saisis librement, en euros ou même en pourcentage, sans aucun contrôle des plafonds légaux : la plateforme ne connaît ni les plafonds au m² par zone (12/10/8 € pour visite-dossier-rédaction, 3 €/m² pour l'état des lieux), ni la règle selon laquelle la part locataire ne peut dépasser la part propriétaire. Un bail d'habitation peut ainsi être signé avec 2 000 € de frais de dossier locataire ou 10 % du loyer — deux montants illégaux — et le système les facture et les comptabilise sans le moindre avertissement. C'est une exposition juridique directe pour l'agence (sanctions, remboursement au locataire).
Aucun plafond ALUR sur les honoraires locataire (visite/dossier/rédaction du bail, état des lieux) ni de contrôle locataire <= propriétaire
Les honoraires locataire de dossier et d'état des lieux sont des valeurs Quantity libres (EUR ou même PERCENTAGE) validées uniquement sur le caractère non nul du montant/unité/type/déclencheurs (InvoiceHelper.isValidFee). Le grep dans la validation adb-contracts (validators/rules du rentalContract) et adb-model ne trouve aucun plafond au m² (12/10/8 EUR par m² par zone pour visite-dossier-rédaction, 3 EUR/m² pour l'EDL), aucune notion de zone, ni aucune règle empêchant la part locataire de dépasser la part propriétaire. Un bail d'habitation peut être signé avec p. ex. des honoraires de dossier locataire de 2000 EUR ou 10 % du loyer — tous deux illégaux — et la plateforme les facture et les comptabilise (411100/RC -> 402100) sans aucune alerte.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/InvoiceHelper.java:177adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.java:219adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:48
Contre-vérification — isValidFee (InvoiceHelper.java:177-186) ne fait que des contrôles de non-nullité ; RentalFeesAmount.java:48-51 accepte EURO ou PERCENTAGE pour les types d'honoraires locataire (isTenantFee, l.75-78), donc des honoraires locataire en pourcentage du loyer sont représentables. Le grep de alur/honoraire/plafond/zone/décret 2014-890 dans tous les modules Java et les deux frontends ne trouve aucune logique de plafonnement — seulement des en-têtes statiques de sections de bail (adb-ui contracts.service.ts:965). La validation du rentalContract ne lit jamais feesAmounts (seulement rentCapped, ReadyToSignValidator.java:219, qui relève de l'encadrement des loyers). Les honoraires sont auto-facturés au premier CFR selon la doc de la page événement. Règle légale correctement énoncée. Sévérité abaissée : absence de garde-fou nécessitant une saisie opérateur, et non une erreur de calcul du moteur.
Les honoraires en pourcentage sont configurés à la création du contrat, et le montant est calculé automatiquement depuis la base CFR (ex. 10 % d'honoraires de dossier locataire produisent 10 % du montant CFR concerné). Pour un forfait, l'utilisateur fournit le montant fixe. Les plafonds ALUR, les restrictions sur les unités d'honoraires et la règle « honoraires locataire <= honoraires propriétaire » ne faisaient pas partie des exigences métier convenues. Ces points devraient d'abord être clarifiés avec le métier avant d'être qualifiés de bugs.
Lorsqu'un contrat est créé à partir d'un modèle, les frais de dossier et de rédaction de bail — des frais censés être facturés une seule fois, à la mise en location — sont paramétrés pour se déclencher à chaque appel de loyer. Comme rien ne limite ce déclenchement au premier mois, une nouvelle facture est émise à chaque échéance : des honoraires de rédaction de bail sont refacturés au locataire ou au propriétaire tous les mois, pendant toute la durée du bail. Ce sont des facturations indues et répétées, avec le risque de réclamations et de remboursements. Le problème ne concerne que les contrats créés via un modèle ; la saisie manuelle utilise le bon déclencheur.
Les honoraires de dossier issus des templates reçoivent le déclencheur CFR, si bien que des honoraires ponctuels se répètent à chaque période de loyer
TemplateValuesServiceImpl (adb-contracts) crée les honoraires de dossier locataire/propriétaire avec eventTriggers = [CFR], alors que la valeur par défaut de l'IHM pour ces mêmes types d'honoraires est CONTRACT_SIGNED et que la spec dit qu'ils se déclenchent « au premier CFR ». CallForRentFeesInvoiceCreationStrategy n'a aucune barrière de première période (grep firstCfr/isFirst : rien) : elle s'exécute à chaque événement CALL_FOR_RENT inséré et sélectionne tout honoraire dont les déclencheurs contiennent CFR. Les clés de déduplication de facture incluent le callForRentPeriod, de sorte qu'une nouvelle facture est créée chaque mois — des honoraires de rédaction de bail ponctuels sont refacturés au locataire/propriétaire à chaque période, pendant toute la durée du bail.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/TemplateValuesServiceImpl.java:339adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/AbstractAccountingEventFeesInvoiceCreationStrategy.java:164
Contre-vérification — Confirmé. TemplateValuesServiceImpl.java:336-344 persiste RENTAL_APPLICATION_FEES_* avec le déclencheur CFR sur RentalContract.rentalContractFeesAmounts (RentalContract.java:515). Chaque CALL_FOR_RENT traité (AccountingFunctions.java:44-49, récurrent selon la doc) exécute CallForRentFeesInvoiceCreationStrategy, dont le filtre (AbstractFeesInvoiceCreationStrategy.java:264) ne se base que sur eventTriggers contenant CFR — aucune barrière de premier CFR n'existe. La clé unique de dédup (AbstractAccountingEventFeesInvoiceCreationStrategy.java:164-176) inclut callForRentPeriod, donc un honoraire EURO forfaitaire est refacturé mensuellement, contredisant la doc (« déclenché au 1er CFR », Récurrent : non) et la valeur par défaut CONTRACT_SIGNED de l'IHM (contract.mapper.ts:809-810). Sévérité maintenue à medium : les factures sont PARKED (validation manuelle) et seul le chemin contrat créé par template est affecté.
Peut-être techniquement valide pour les contrats issus de templates, mais cela relève d'un flux futur incomplet. La priorité initiale portait sur les contrats de reprise créés via l'ancienne UI ; les nouveaux contrats par template n'étaient pas censés être pleinement utilisés à ce stade, leur logique d'honoraires ponctuels n'a donc pas été finalisée.
Toutes les écritures d'honoraires sont enregistrées dans le journal des Achats, alors que la documentation comptable de la plateforme prévoit un journal Honoraires dédié — qui ne reçoit en réalité jamais aucune écriture. Les rapports par journal et les états destinés à l'expert-comptable ne reflètent donc pas la réalité : les honoraires sont noyés dans les achats. Par ailleurs, d'anciennes écritures d'honoraires enregistrées sous l'ancien format ne peuvent plus être relues par le système, ce qui provoquerait une erreur si elles étaient consultées.
Les écritures d'honoraires sont comptabilisées au journal Achats, et non Honoraires ; les événements d'honoraires documentés sont inatteignables et non mappables
Toutes les factures d'honoraires sont comptabilisées via l'événement générique OUTGOING_AGENCY_INVOICE, et InvoicePostingRuleFactory estampille chaque écriture (y compris les lignes d'honoraires 622600/622610/622620/622630/622640) avec eventType.getJournal() = BUY (Achats). La matrice documentée et l'enum affectent les événements RENTING_*_FEES au journal FEES (Honoraires, journal n°4), mais les classes d'événements par honoraire sont toutes @Deprecated(forRemoval=true) sans constructeur vivant, et le switch de AbstractAccountingEventMapper.fromEntity n'a aucun cas RENTING_* — tout événement d'honoraire hérité persisté tombe sur `default -> throw new IllegalStateException`. Effet net : le journal Honoraires ne reçoit jamais d'écritures du chemin d'honoraires actuel ; le reporting au niveau journal et les JournalCode/JournalLib du FEC divergent de la spec documentée, et les événements d'honoraires historiques ne peuvent être relus.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/InvoicePostingRuleFactory.java:93adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java:92adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/mapper/AbstractAccountingEventMapper.java:263
Contre-vérification — Re-dérivé : les factures d'honoraires sont des factures sortantes (AbstractBookInvoiceAction.java:83 mappe OUTGOING_DIRECTION -> OUTGOING_AGENCY_INVOICE ; Invoice.java:235), et InvoicePostingRuleFactory.java:79/93 estampille chaque ligne, y compris les items d'honoraires 622xxx, avec eventType.getJournal() = BUY (AccountingEventType.java:92). La doc (events/renting-application-owner-fees-without-vat.md, journals.md journal 4) et l'enum (:96-113) affectent FEES/Honoraires. Les sept classes Renting*Fees sont @Deprecated(forRemoval=true) sans aucun constructeur vivant ; le switch de AbstractAccountingEventMapper.fromEntity (~:235-266) manque les cas RENTING_*, donc les entités héritées tombent sur l'IllegalStateException par défaut. La sévérité medium est justifiée : mauvaise classification de journal, non une erreur monétaire.
Résultat d'une refonte intentionnelle. Auparavant chaque honoraire avait son propre événement comptable, modèle jugé incorrect. Dans l'implémentation actuelle, tous les honoraires sont regroupés sous l'événement de facture OUTGOING_AGENCY_INVOICE, chaque honoraire produisant sa propre écriture et son imputation au grand livre. Les anciens événements d'honoraires spécialisés ne sont donc plus utilisés, volontairement.
Toutes les factures d'honoraires automatiques appliquent une TVA figée à 20 % : impossible d'appliquer un taux réduit (8,5 % en outre-mer), la franchise en base, ou de suivre un changement de taux légal sans intervention technique. Par ailleurs, le montant de TVA est calculé sur la facture mais jamais enregistré dans un compte de TVA : les comptes de TVA collectée/déductible existent dans le plan comptable mais restent vides. La TVA collectée sur les honoraires est donc introuvable dans le grand livre et dans le FEC, ce qui complique les déclarations de TVA et fragilise l'agence en cas de contrôle fiscal.
TVA codée en dur à 20 % et jamais comptabilisée sur un compte de TVA (445710 inutilisé)
Chaque facture d'honoraires automatique applique une TVA de 20 % codée en dur (VAT_VALUE = 0.2) : item.amount = HT + 20 % (TTC) et item.taxesAmount = HT*0.2. Mais à la comptabilisation, InvoicePostingRuleFactory passe item.amount (TTC) au compte de charge/locataire et le total TTC au 402100 — taxesAmount est stocké sur l'item de facture et ne devient jamais une écriture au grand livre. coa.csv définit 445510/445660/445710 (TVA à décaisser/déductible/collectée) mais le grep ne trouve aucune référence de code à ces comptes ni à leurs libellés VAT_IN/VAT_OUT. Conséquences : la TVA collectée sur honoraires est intraçable dans le grand livre/FEC ; les taux réduits (DOM 8,5 %), la franchise en base ou un changement de taux réglementaire sont impossibles sans modification de code. Les factures manuelles laissent l'IHM fixer un vatRate par ligne, mais le chemin des honoraires automatiques ne le peut pas.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/InvoiceHelper.java:67adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/InvoicePostingRuleFactory.java:91adb-accounting/src/main/resources/coa.csv:35
Contre-vérification — Faits re-dérivés : InvoiceHelper.java:67 code en dur VAT_VALUE=0.2 ; AbstractFeesInvoiceCreationStrategy.java:126-127 fixe le montant de l'item en TTC ; InvoicePostingRuleFactory.java:88-104 ne comptabilise que item.getAmount() — getTaxesAmount() n'atteint jamais une écriture au grand livre ; le grep sur tout le dépôt montre que 445510/445660/445710 (coa.csv:33-35) ne sont référencés nulle part dans le code. Toutefois, la matrice d'événements documentée (events/renting-management-owner-fees-without-vat.md : 622640/402100, sans ligne de TVA) spécifie exactement cette comptabilisation TTC — c'est un choix délibéré de comptabilité mandant où la TVA collectée relève des livres propres de l'agence, et les items de facture portent bien taxesAmount/vatRate pour les mentions de facture. Rétrogradé en low : conforme à la spec ; le risque résiduel est le taux codé en dur et la traçabilité de la TVA au niveau du grand livre.
Le taux de TVA à 20 % a été volontairement codé en dur pour la première étape d'implémentation. Un modèle de taxe souple était prévu pour plus tard.
Le système sépare bien les frais à la charge du locataire et ceux à la charge du propriétaire en deux factures distinctes, mais pour les frais déclenchés à l'appel de loyer, les deux factures sont identifiées de la même façon et le destinataire est toujours le propriétaire. Résultat : quand des frais locataire sont déclenchés à l'appel de loyer (ce que produisent justement les contrats créés à partir d'un modèle), les deux factures se confondent et les frais du locataire atterrissent sur un document adressé au propriétaire. En comptabilité, c'est bien le locataire qui est débité, mais le document facture est envoyé à la mauvaise personne — le locataire n'a pas de justificatif et le propriétaire reçoit une facture qui ne le concerne pas.
L'identité de la facture d'honoraires d'origine CFR ignore la répartition locataire/propriétaire ; les honoraires locataire finissent sur une facture adressée au propriétaire
filterFees répartit délibérément les honoraires déclenchés en une liste locataire et une liste propriétaire (deux factures), mais pour les origines CFR/lettrage la clé d'unicité est construite à partir du propriétaire + période uniquement, et buildHeader fixe toujours receiver = descriptor.getOwner(). Invoice.generateCfrOriginKey clé également sur propriétaire + période. Si des honoraires côté locataire portent le déclencheur CFR (exactement ce que produit TemplateValuesServiceImpl), la facture locataire et la facture propriétaire entrent en collision sur la même clé unique et les items d'honoraires locataire sont rattachés à une facture dont le destinataire est le propriétaire — le locataire est débité au grand livre (compte d'item 411100/RC) alors que le document de facture est adressé au propriétaire.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/AbstractAccountingEventFeesInvoiceCreationStrategy.java:164adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Invoice.java:401
Contre-vérification — Re-dérivé de bout en bout. TemplateValuesServiceImpl.java:339 (chemin IHM vivant : POST /contracts/templateValues, adb-ui contracts.service.ts:1344) crée des honoraires de dossier locataire avec le déclencheur CFR. Au 1er CFR, filterFees (AbstractFeesInvoiceCreationStrategy:232-254) émet les listes locataire et propriétaire, mais unique() (AbstractAccountingEventFeesInvoiceCreationStrategy:164-176) clé sur propriétaire+période uniquement et buildHeader:110 force receiver=propriétaire ; saveIfNotExists (InvoiceServiceImpl:301-313) fusionne la seconde liste à clé dupliquée dans la première facture via generateCfrOriginKey (Invoice.java:401-412). La doc renting-application-tenant-fees-without-vat.md indique « au locataire (déclenché au 1er CFR) » ; la classe sœur ContractSignedFeesInvoiceCreationStrategy:107,114 montre la répartition prévue. Le grand livre débite toujours le locataire (411100), donc la sévérité reste low.
La règle de regroupement ne repose pas directement sur une distinction propriétaire/locataire. Les honoraires sont routés selon leur déclencheur configuré : un honoraire ayant un déclencheur donné est ajouté au flux de facture correspondant.
L'onglet « Extrait de comptes » d'un bail plante en JS (TypeError sur 'PART_NATURE') et laisse un spinner infini alors que les 3 appels API répondent 200.
Bail RC-00000004 : « prochaine révision 10/01/2025 » dépassée de 18 mois, sans alerte. Indexation bloquée par « Aucun diagnostic » (garde-fou DPE présent, blocage jamais signalé).
Des baux à 24 377 € et 44 451 € de solde débiteur sans niveau de relance ni date — escalade entièrement à l'initiative de l'opérateur.
Numéros de mandats libres et non séquentiels (1122, 3601, 145609…) — incompatible loi Hoguet (easyId libre confirmé).
La carte « Persons » de l'accueil affiche deux fois « Ajouter un propriétaire ».
622620 et 622640 portent le même libellé « Honoraires - gestion courante ».
Prise de mandat — Rédaction du mandat de gestion (type, durée, honoraires) · Rattachement immeuble / lot au mandat · Le mandat rend les lots gérables (prérequis bail)
Mise en location — Appel du dépôt de garantie · Premier appel de loyer (1er quittancement + honoraires)
Quittancement mensuel — Échéance de la période / détermination des contrats à quittancer · Génération de l'appel de loyer (loyer + charges + TEOM) et écritures
Révision annuelle des loyers (IRL) — Publication de l'indice INSEE / ingestion · Calcul du nouveau loyer révisé · Prise d'effet du nouveau loyer · Quittancement suivant utilisant le nouveau loyer
Régularisation annuelle des charges — Collecte des factures fournisseurs sur l'année
Reversement propriétaire & CRG — Calcul des honoraires de gestion
Travaux, sinistres & fournisseurs — Paiement du fournisseur
Sortie locataire (congé → solde de tout compte) — Date de sortie fixée
Cycle comptable annuel — Écritures courantes
Signature du mandat Prise de mandat — Le PDF du mandat imprime un placeholder brut `${}` au lieu du numéro.
Ouverture du compte auxiliaire propriétaire (466100) Prise de mandat — Compte auxiliaire créé avec easyId null si le numéro n'est pas saisi.
Contrats en état ERROR pendant le run Quittancement mensuel — NPE DG null → bloque tout le 1er appel (F047) ; aucune reprise produit
Prélèvement SEPA généré Quittancement mensuel — pain.008 sans PmtTpInf → rejetable banque ; pas de FRST/RCUR ; pas de rejets
Encaissement et comptabilisation du règlement Quittancement mensuel — SEPA auto-comptabilisé à la génération du fichier, jamais lettré (F055)
Détection des impayés / relance Quittancement mensuel — relances enregistrées comme envoyées, envoi commenté (F084 critique)
Relance 1 (rappel simple) Impayés → relances → contentieux → GLI — Le courrier L1 facture les relances au locataire — clause illégale, art. 4 loi 89 (F087).
Mise en demeure (LRAR) Impayés → relances → contentieux → GLI — Bailleur « Société V FUND IMMO CORE » codé en dur (F088) ; la chaîne s'arrête à L3.
Livraison effective des relances (email / courrier / LRAR) Impayés → relances → contentieux → GLI — Marquées envoyées, jamais parties : emailService.send commenté (F084).
Reversement de l'indemnité au propriétaire Impayés → relances → contentieux → GLI — GLI_TO_OWNER inatteignable + journal absent du seed prod (F076, critique).
Déduction des factures fournisseurs et travaux Reversement propriétaire & CRG — Paiement fournisseur sur mandat : banque créditée 2 fois, bailleur double-débité (F061 critique).
Dernier loyer au prorata Sortie locataire (congé → solde de tout compte) — Dernière période jamais facturable pour les baux trimestriels/annuels à terme échu (F045).
Restitution du DG sous 1 ou 2 mois Sortie locataire (congé → solde de tout compte) — Remboursement DG non gardé : ni détenteur, ni solde, ni plafond (F002) ; échéance calculée jamais alertée.
Lettrage Cycle comptable annuel — Recherche aveugle au sens : dettes rouvertes par contre-passation invisibles (F053).
Mise en location — Annonce et diffusion sur les portails
Impayés → relances → contentieux → GLI — Information de la caution / du garant
Régularisation annuelle des charges — Mise à disposition des justificatifs au locataire (1 mois)
Reversement propriétaire & CRG — Provisions / retenues propriétaire (retenue de trésorerie) · Archivage du relevé propriétaire (envoi / archivage)
Travaux, sinistres & fournisseurs — Accord du propriétaire (seuil)
Sortie locataire (congé → solde de tout compte) — Comparaison avec l'EDL d'entrée · Pénalité de retard 10 %/mois
Fin de mandat (sortie propriétaire) — Préavis / dénonciation du mandat au propriétaire · Dépôts de garantie transférés au nouveau gestionnaire · Comptes locataires transférés (sortie) · Clôture du compte propriétaire · Désactivation des lots
Cycle comptable annuel — Rapprochement bancaire (import de relevés) · À-nouveaux / report à nouveau · Export FEC · Déclaration de TVA (CA3) · DAS2 (honoraires versés à des tiers) · Relevé propriétaire / revenus fonciers (2044-2072)
Obligations annuelles & conformité documentaire — Alerte / relance avant expiration d'un document réglementaire · Souscription par le bailleur pour le compte du locataire (+10%) · Attestation de loyer pour la CAF
Prise de mandat — Création du propriétaire (prospection) · Inscription au registre des mandats (numéroté, chronologique, non modifiable) · Coordonnées bancaires propriétaire (RIB) · Mandat actif et facturable (honoraires de gestion) · Suivi échéance / reconduction du mandat
Mise en location — Mise en vacance du lot · Fixation du loyer (encadrement / loyer de référence) · Réception des candidatures · Dossier + solvabilité (3x loyer, pièces décret 2015-1437) · Sélection du locataire · Garant / caution (Visale) · Rédaction du bail depuis un modèle · Signature · EDL d'entrée (état des lieux) · Attestation d'assurance locataire (MRH)
Quittancement mensuel — Déclenchement du run mensuel (batch, preview + rapport d'exceptions) · Avis d'échéance produit (document locataire) · Envoi de l'avis d'échéance au locataire · Lettrage du règlement contre l'appel · Émission de la quittance (seulement si payé)
Impayés → relances → contentieux → GLI — Détection de l'impayé (solde, seuil, jours) · Relance 2 (menace de procédure) · Commandement de payer par huissier · Assignation / mise en oeuvre de la clause résolutoire · Déclaration de sinistre GLI à l'assureur · Encaissement de l'indemnité GLI · Créance locataire maintenue ouverte (subrogation) · Remboursement par le locataire · Remboursement de l'assureur (trop-perçu / récupération) · Passage en perte / créance irrécouvrable
Révision annuelle des loyers (IRL) — Identification des baux éligibles (anniversaire, indice, gel DPE, encadrement) · Campagne : prévisualisation, contrôle, rollback · Notification au locataire (avis de révision) · Restitution des baux non révisés / rapport d'anomalies
Régularisation annuelle des charges — Classification récupérable / non récupérable (décret 87-713) · Ventilation aux lots via tantièmes / clés de répartition · Calcul réel vs provisions par locataire, prorata d'occupation · Régularisation facturée ou créditée au locataire · Re-calibrage des provisions pour l'année suivante
Reversement propriétaire & CRG — Clôture de la période CRG · Identification des loyers réellement encaissés · Calcul du solde propriétaire · Production du document CRG (reddition des comptes) · Paiement du propriétaire (reversement / virement) · Périodicité / planification du prochain CRG
Travaux, sinistres & fournisseurs — Signalement d'incident (locataire/propriétaire) · Création du ticket · Devis demandé au fournisseur · Émission de l'ordre de service · Exécution / intervention · Facture fournisseur reçue et comptabilisée · Classification récupérable vs charge propriétaire · Refacturation au locataire (part récupérable) · Apparition dans le CRG · Déclaration de sinistre (assurance)
Sortie locataire (congé → solde de tout compte) — Congé reçu (locataire ou bailleur) · Préavis calculé et validé · EDL de sortie · Retenues justifiées sur dépôt de garantie · Régularisation des charges sur année partielle (ou retenue 20 % jusqu'à la régul. copro) · Arrêté des comptes / solde de tout compte · Bail clôturé
Fin de mandat (sortie propriétaire) — Résiliation du mandat · CRG final / arrêté de compte · Soldes apurés · Remise des documents
Cycle comptable annuel — Clôture de période (verrouillage) · Clôture d'exercice · Archivage et inaltérabilité
Obligations annuelles & conformité documentaire — Réception & archivage de l'avis de taxe foncière / TEOM · Refacturation de la part récupérable aux locataires (campagne de masse) · Suivi de validité des diagnostics (DPE, ERP, amiante, plomb, élec/gaz) · Collecte annuelle de l'attestation d'assurance MRH du locataire · Quittances / avis d'échéance à la demande · Assurance PNO (propriétaire non occupant)
Le numéro de mandat : rien ne le génère (ContractStateServiceImpl ne sérialise que les RentalContracts, contrôle easyId RDC commenté, aucune config de série) — pas de registre des mandats, PDF avec placeholder `${}`, compte 466100 ouvert sans numéro.
La création d'un propriétaire fonctionne bien : la fiche propriétaire existe en tant qu'objet à part entière et l'on peut lancer un mandat directement depuis la fiche du propriétaire ou du lot, sans ressaisie. En revanche, la plateforme ne gère aucune phase de prospection : il n'existe ni pipeline commercial ni statut « prospect » — un propriétaire existe ou n'existe pas. Le suivi des contacts en amont (rendez-vous, relances commerciales, estimation) doit donc se faire hors outil, typiquement dans un tableur ou un agenda.
Le CRUD propriétaire vit dans adb-persons et adb-ui (owner-rdclist.component.ts) ; tabs.service.ts:161 route vers `/lists/mandates/create-rental-delegate/${ownerId}/${partId}`, donc ownerId+partId sont propagés automatiquement dans le wizard de mandat. Un grep repo-wide sur "prospect" ne remonte qu'un libellé codé en dur dans tenants.component.ts:172 — aucun objet ni écran de prospection n'existe ; s'il devait exister, il vivrait plausiblement dans adb-persons + un composant liste dédié côté adb-ui.
adb-ui/src/app/content/components/persons/owners/owner-rdclist/owner-rdclist.component.tsadb-ui/src/app/core/services/shared/tabs.service.ts:161adb-ui/src/app/content/components/tenants/tenants.component.ts:172
La rédaction du mandat de gestion est le point fort de la chaîne : le wizard couvre l'ensemble des conditions françaises — honoraires de gestion, d'entremise, frais de dossier locataire et bailleur, état des lieux, durée avec calcul de la date de fin, tacite reconduction, médiateur, fréquence et mode d'envoi du CRG, RIB. Tout est contrôlé avant que le mandat puisse passer au statut « prêt à signer ». Rien à ressaisir, rien à surveiller ici.
RentalDelegateContract.java porte l'ensemble du modèle (category, duration, tacitRenewal, accountStatementFrequency, paymentMethod, plannedPayDate, managementFeesBasis, rentalDelegateContractAmounts, specificClauses, ombudsPerson, accountingStatementSendingMethod, bankAccount). Côté UI, le wizard rd-step1/rd-step2/rd-step3 plus les templates partagés rdc-conditions couvrent la saisie ; RentalDelegateContractReadyToSignValidator.java valide le tout avant la transition vers READY_TO_SIGN.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalDelegateContract/RentalDelegateContractReadyToSignValidator.java
La signature du mandat existe mais elle est défaillante. Il n'y a pas de signature électronique : le gestionnaire saisit une date et téléverse un scan du mandat signé sur papier. Surtout, le PDF de mandat remis au propriétaire est cassé à l'endroit le plus sensible : la ligne « N° de mandat » affiche un fragment de code brut au lieu du numéro. Un document contractuel non conforme part donc chez le propriétaire, avec un risque d'image et un risque juridique au regard de la loi Hoguet, tant que le document n'est pas corrigé à la main.
La signature est un SignContractModel plus un upload FILE_TYPE.RENTAL_DELEGATE_CONTRACT_LIFECYCLE dans rdc-change-status-to-active.component.ts, traité via ContractSignEventInfo.java et ContractStateServiceImpl.java — aucune intégration e-signature. Bug confirmé : le template PDF adb-reports/src/main/resources/templates/RDC.html:473 contient le placeholder non résolu `N° de mandat: ${} easyid externalId?`, rendu tel quel dans le PDF ; aucune valeur n'est injectée à la génération.
adb-ui/src/app/content/components/rentals-delegates/display-rental-delegate-contract/modals/rdc-change-status-to-active/rdc-change-status-to-active.component.tsadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/event/ContractSignEventInfo.javaadb-reports/src/main/resources/templates/RDC.html:473
Le registre des mandats, obligation de la loi Hoguet, n'est que partiellement couvert. Un numéro de mandat peut bien être saisi : c'est un champ obligatoire avant le passage en « prêt à signer », il s'imprime sur le PDF et apparaît en colonne triable dans la liste des mandats. Mais c'est une saisie manuelle : rien ne garantit la séquence chronologique, l'unicité ni l'inaltérabilité du numéro, qui reste modifiable après coup, et aucune vue ou export « registre des mandats » n'existe. En cas de contrôle, le registre réglementaire doit être reconstitué à la main.
Le numéro existe comme saisie manuelle : rdc-draft-state.component.html:123-131 exige un input numérique (data-cy=contract-number) avant DRAFT→READY_TO_SIGN, stocké via ContractStateServiceImpl.java:335-340 `updateEasyIdIfApplicable` (ContractFinalizeOfferEventInfo.getEasyId()), rééditable via rdc-change-status.component.html:21 et imprimé dans rentalDelegateContract.ftlh:473-479. Mais aucune séquence automatique : ContractStateServiceImpl.java:441-451 `generateAndSetContractEasyId` sort tôt sauf `isRentalContract(contract)`, ContractSerialGenerator.java est codé en dur sur RENTAL_CONTRACT_DOCUMENT, infra/seeds configurations.json n'a aucune config serial RDC, et le contrôle « has easyId » de RentalDelegateContractReadyToSignValidator.java:53-58 est commenté ; contract.mapper.ts:449 retombe sur `mandateNumber = "-"`. Ni unicité, ni immutabilité, ni export chronologique.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:335-340adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractSerialGenerator.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalDelegateContract/RentalDelegateContractReadyToSignValidator.java:53-58adb-ui/src/app/core/mappings/contract.mapper.ts:449
Le rattachement des immeubles et lots au mandat fonctionne très bien : la plateforme vérifie que les lots appartiennent réellement au propriétaire, applique les règles copropriété / mono-propriété, refuse un lot déjà couvert par un autre mandat signé, et rattache automatiquement tout nouveau lot créé au mandat signé du propriétaire. C'est le maillon le plus solide de la chaîne, sans intervention manuelle.
CommonContractValidator.java:351-417 `validatePartForRentalDelegateContract` implémente les règles copro vs mono-propriété (NOT.ALLOWED.RDC.FOR.COPROPERTY.BUILDING.KEY) et le contrôle de doublon de RDC ; ContractServiceImpl.java:308 `attachPartToRentalDelegateContract` pousse automatiquement les nouveaux lots dans l'unique RDC signé du propriétaire. La sélection UI vit dans rdc-contract-step2-component.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/CommonContractValidator.java:351-417adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:308
Le RIB du propriétaire est bien géré sur sa fiche personne, mais son rattachement au mandat est un détour manuel : ce n'est pas une étape du wizard de mandat. Le gestionnaire doit quitter le wizard, créer le RIB sur la fiche du propriétaire, puis rouvrir le mandat en brouillon pour l'y attacher — et rien n'impose qu'un RIB soit présent avant l'activation du mandat. Une fois le mandat actif, le RIB n'est plus visible depuis le mandat. Risque concret : un mandat actif sans coordonnées bancaires, découvert seulement au moment de reverser les loyers au propriétaire.
RentalDelegateContract.java porte un champ `bankAccount` (BankContract), alimenté par un PATCH séparé : ContractController.java:240 → ContractServiceImpl.java:496 `patchBankAccount`. Côté UI, la saisie du RIB vit dans person-details/info-tabs/bank-modals ; le bloc shared/contracts/bank-info-bloc n'est référencé que dans rdc-draft-state.component.html:51 et absent de tous les templates rdc-active-state ; aucun champ IBAN dans le wizard rentals-delegates. Aucun validateur n'exige un bankAccount avant activation.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/ContractController.java:240adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:496
À la signature du mandat, la plateforme ouvre bien automatiquement le compte auxiliaire du propriétaire sous le compte collectif 466100, sans écriture manuelle — le principe est le bon. Mais le numéro de ce compte est repris du numéro de mandat : si celui-ci n'a pas été saisi (rien ne l'impose de bout en bout), le compte auxiliaire est créé avec un numéro vide. Conséquence comptable directe : des comptes propriétaires sans identifiant exploitable, difficiles à pointer, à lettrer et à justifier lors d'un contrôle des fonds mandants.
ContractsFunctions.java (onContractModified, message SQS filtré sur l'état SIGNED) déclenche createOrUpdateAuxAccCallBack ; LedgerAuxiliaryAccountServiceImpl.java:412 accepte RentalDelegateContract, :445 met la description "OWNERS", et :470 construit le numéro via `ledgerAccountShort(parent, contract.getEasyId(), …)`. Or easyId n'est jamais généré pour un RDC (saisie manuelle non obligatoire de bout en bout) et il n'existe aucun générateur de repli, contrairement à `supplierAccountNumber(...)` pour les fournisseurs — d'où un compte auxiliaire à numéro null/vide au lieu du RDC-xxx documenté dans auxiliary-accounts.md.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/LedgerAuxiliaryAccountServiceImpl.java:470adb-doc/docs/concepts/accounting/auxiliary-accounts.md
Une fois actif, le mandat rend bien les lots louables, mais il ne pilote pas lui-même la facturation : la grille d'honoraires négociée (gestion, entremise, état des lieux) doit être ressaisie à la main sur chaque bail, où elle est ensuite facturée. De même, la fréquence du CRG et la date de versement saisies sur le mandat sont enregistrées puis ignorées par le moteur de CRG. Double conséquence : du travail de ressaisie bail par bail, et un risque d'écart entre les honoraires promis au propriétaire dans le mandat et ceux réellement facturés.
Côté facturation, ContractsFunctions.java `createInvoice` filtre `contract instanceof RentalContract` et AbstractFeesInvoiceCreationStrategy.java:149 lit `RentalContract::getRentalContractFeesAmounts` — jamais les montants du RDC. L'API ContractController.java:172 `/rentalDelegateContract/fees` existe mais un grep adb-ui sur "/fees", "rdcFees", "getRdcFees" ne trouve aucun appelant. `accountStatementFrequency` est stocké/validé partout (modèle, entité, agrégat) mais jamais lu par le code CRG (CRGPeriodRecapFactoryImpl.java ne l'utilise pas).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/AbstractFeesInvoiceCreationStrategy.java:149adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/ContractController.java:172
Le verrou fonctionne parfaitement : impossible de valider ou signer un bail si aucun mandat signé ne couvre ce propriétaire et ces lots (y compris l'immeuble parent en mono-propriété). C'est un enchaînement réellement contrôlé par la plateforme — aucun bail ne peut être créé sans mandat en règle, ce qui protège l'agence sur le plan réglementaire.
CommonContractValidator.java:571-620 `validateRelatedRDCAndUpdateContract` rejette la validation avec NO.RELATED.SIGNED.RDC.EXISTS.FOR.OWNER.AND.PARTS.KEY si aucun RDC signé ne couvre le propriétaire et les lots ; la requête sous-jacente est ContractRepository.java:217 `findSignedRentalDelegateContractsForOwnerAndPart`.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/CommonContractValidator.java:571-620adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/ContractRepository.java:217
La date de fin du mandat est calculée et affichée, et la tacite reconduction est enregistrée, mais aucun suivi d'échéance n'existe : pas d'alerte, pas de liste des mandats arrivant à expiration, pas de rapport, et la tâche planifiée de renouvellement ne concerne que les loyers des baux, pas les mandats. Un mandat peut donc expirer sans que personne ne soit prévenu, alors que les lots restent loués et facturés — l'agence gérerait alors sans mandat valide, une exposition juridique sérieuse. Le suivi des échéances doit aujourd'hui être tenu à la main, hors outil.
ContractRenewalJobExecutor.java + ContractServiceImpl.java:714-757 `renewContracts` filtrent `.filter(ac -> ac instanceof RentalContract)` et travaillent sur `nextRentRenewalDate` — les RDC sont explicitement exclus. L'UI (rdc-active-state/rdc-end-date) n'affiche que `contract?.end`. Le framework de notifications d'adb-utilities (NotificationDefinitionEntity) existe mais aucun NOTIFICATION_TYPE d'expiration de contrat n'est seedé (grep sur tous les json/java) ; grep expir/échéance sous rentals-delegates et le dashboard : rien. Une alerte d'échéance vivrait plausiblement là.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/schedule/impl/ContractRenewalJobExecutor.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:714-757
La passation sélection -> bail. "Candidature acceptée" est un libellé sans code (zéro référence Java/TS), aucun contrat ne porte de ticketId : candidat retenu, loyer convenu et dépôt sont ressaisis à la main.
Le repérage d'un lot vacant devrait se déclencher tout seul dès qu'un bail se termine. Aujourd'hui la plateforme sait bien afficher un indicateur « Dont vacants » et filtrer la liste des lots sans bail signé, mais il n'existe ni période de vacance, ni suivi comptable de la vacance, et surtout la fin d'un bail (congé) ne crée rien : un gestionnaire doit remarquer le départ et ouvrir lui-même un ticket de recherche de locataire. Le risque concret est un lot qui reste vide plus longtemps que nécessaire simplement parce que personne n'a lancé la démarche.
La vacance n'est pas un état du Part : c'est un tag posé sur un ticket créé manuellement (TICKET_TAG.VACANCY dans adb-ui/src/app/core/mappings/tickets.mapper.ts:356,533,701 ; vacancyTicketType dans unitDTO.ts:57). adb-views dérive une vacance de l'état du bail : PartRepository.getDashboardData:657-666 compte les lots sans rentalContractId ou avec state != SIGNED, et PartSearchRepositoryImpl.rentalContractStateFilter:353-368 alimente le filtre WITHOUT_RENTAL_CONTRACT_STATE (adb-ui grr.service.ts:203). Aucune période de vacance ni écriture comptable associée, et adb-contracts n'a aucune notion de ticket (grep 'ticket' dans adb-contracts/src/main -> 0 hit) : les transitions NOTIFY/END ne déclenchent rien.
adb-ui/src/app/core/mappings/tickets.mapper.ts:356adb-ui/src/app/core/models/parts/unitDTO.ts:57adb-ui/e2e/cypress/integration/CreateVacancy.feature
Fixer le loyer d'un lot en zone tendue suppose de vérifier l'encadrement des loyers : loyer de référence, plafond majoré, éventuel complément de loyer. La plateforme ne capture qu'une case oui/non « loyer encadré » et le loyer de l'ancien locataire, uniquement pour les imprimer dans le bail. Aucun contrôle ne vérifie que le loyer demandé est légal : la conformité repose entièrement sur la vigilance du gestionnaire, avec un risque juridique réel (loyer contestable par le locataire). De plus, le loyer, les charges et le dépôt saisis sur le ticket de recherche de locataire ne sont pas repris : le gestionnaire les retape dans le bail.
RentalContract.java:121 ne porte que rentCapped, previousRent, previousRentLastPaymentDate, previousRentLastReviewDate, exposés dans rc-conditions.component.html:651-671 pour le PDF du bail. Aucune grille zone/loyer de référence, aucun plafond majoré/minoré, aucun complément de loyer : grep -i 'encadrement|loyer de référence|referenceRent|complément de loyer' sur adb-ui/adb-contracts/adb-model -> 0 hit. Les rentAmount/chargeAmount/rentDepositAmount de TenantSearchTicketDetails.java (adb-tickets) ne sont jamais propagés vers le wizard de bail : ressaisie manuelle des trois montants.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:121adb-ui/src/app/contracts/shared/rc-conditions/rc-conditions.component.html:651
La diffusion de l'annonce (texte, photos, publication sur les portails type SeLoger ou Leboncoin) n'existe tout simplement pas dans la plateforme. Il n'y a ni fiche annonce, ni dossier photos, ni export vers un portail : toute la commercialisation se fait forcément avec des outils externes, et rien de ce qui s'y passe (annonce publiée, canal d'origine des candidats) ne remonte dans le système.
Aucune entité annonce ni flux de syndication dans le monorepo : grep repo-wide sur advert, annonce, publication, portal, leboncoin, seloger, diffusion, listing ne retourne que le 'portal' de l'Angular CDK et des correspondances hors sujet. Si la fonctionnalité existait, elle vivrait vraisemblablement côté adb-tickets (ticket de recherche de locataire) ou dans un service dédié absent du repo.
La réception des candidatures devrait constituer un vivier de candidats identifiés avec leur statut. En pratique, une candidature n'est qu'une ligne d'historique sur le ticket de recherche de locataire : un libellé (« Candidature reçue », « En cours de validation », « Acceptée »), une date et un commentaire libre. Le candidat n'a pas d'identité dans le système, pas de statut individuel, pas de classement : impossible de savoir combien de candidats sont en lice ni où en est chacun sans lire les commentaires un par un.
Pas d'entité candidature/dossier : les libellés APPLICATION_RECEIVED / UNDER_VALIDATION / ACCEPTED ne sont que des entrées de catalogue (adb-tickets/src/main/resources/CATALOG_fr.properties:2142-2144) portées par Event.java (eventType = CatalogValue libre + comment + date). Aucun code ne réagit à ces valeurs : grep -rn 'APPLICATION_ACCEPTED|APPLICATION_RECEIVED' --include=*.java --include=*.ts -> 0 hit. Le candidat n'est même pas une Person à ce stade.
adb-tickets/src/main/resources/CATALOG_fr.properties:2142
L'étude du dossier locataire devrait vérifier la complétude des pièces autorisées (décret 2015-1437) et la solvabilité (règle usuelle des revenus à 3 fois le loyer). La plateforme permet seulement d'attacher des PDF typés « identité », « revenus », « justificatif de domicile » au ticket ; il n'y a ni liste de pièces obligatoires, ni contrôle de complétude, ni calcul du taux d'effort, ni notation. La décision de sélection ne repose donc que sur l'analyse personnelle du gestionnaire, sans trace structurée en cas de contestation ou de refus mal documenté.
Seuls des uploads génériques existent : FILE_TYPE.IDENTITY / INCOME / PROOF_RESIDENCE (adb-utilities/DOCUMENTS-values-input.tsv:1721-1730) et FILE_TYPE.TICKET_TENANT_SEARCH (:1743), attachés au ticket ou à un event via create-event-modal.component.ts:318-319 (adb-ui). Aucune checklist décret 2015-1437, aucun calcul de ratio : grep -i 'solvab|payslip|incomeRatio|taxNotice|3x' -> rien. Aucun verdict calculé n'existe ; la décision vit dans un commentaire libre.
adb-utilities/DOCUMENTS-values-input.tsv:1721adb-ui/src/app/tickets/create-event-modal/create-event-modal.component.ts:318
Le choix du locataire retenu devrait comparer les candidats et déclencher la suite (création du bail). Le ticket ne peut désigner qu'un seul locataire : impossible de représenter plusieurs candidatures concurrentes, de les comparer ou de tracer les refus. Et « Candidature acceptée » n'est qu'une étiquette sans effet : accepter le candidat ne crée ni bail, ni brouillon, ni même une fiche personne. Le gestionnaire repart de zéro dans le module contrats, sans lien vers le ticket : la suite ne reçoit rien automatiquement.
TicketDescriptor.java (adb-tickets) ne porte qu'un TargetObject 'tenant' unique (plus parts/owner/rentalDelegateContract) : les candidats concurrents ne sont pas représentables. APPLICATION_ACCEPTED (CATALOG_fr.properties:2142) n'a aucun handler : TicketServiceImpl ne traite que les transitions CREATION/CLOSED/REOPENED/NOTIFIED. L'acceptation ne crée ni RentalContract ni Person, et aucun champ du contrat ne référence le ticket d'origine.
adb-tickets/src/main/resources/CATALOG_fr.properties:2142
La caution est réellement gérée : il existe un contrat de garantie à part entière, et le bail ne peut pas passer « prêt à signer » tant qu'une garantie liée n'est pas signée — c'est le seul vrai enchaînement automatique de la phase pré-bail. En revanche la garantie se résume à un montant et un nombre de renouvellements : pas d'étude de solvabilité du garant, pas de génération de l'acte de cautionnement (à rédiger hors plateforme), et aucune prise en charge de Visale, pourtant très courant. La GLI, elle, est couverte par un contrat d'assurance dédié.
GuaranteeContract.java + GuaranteeCondition.java (adb-model) ne portent que amount et renewalCoveredNumber ; GuaranteeContractServiceImpl.java:198 gère la transition SIGN, et CommonContractValidator.java:471-501 (validateGuaranteeForRentalContract) bloque le READY_TO_SIGN du bail si une garantie liée n'est pas signée. ContactCategory.java:11 définit GUARANTOR ; la GLI passe par InsuranceContract + les événements comptables GLI_*. grep -ri 'visale' repo-wide -> 0 hit ; aucune génération d'acte de cautionnement.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/GuaranteeContract.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/GuaranteeContractServiceImpl.java:198adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/CommonContractValidator.java:471
La rédaction du bail est le point fort de la chaîne : un vrai moteur de modèles génère le bail (civil ou commercial) avec une bibliothèque de clauses en français et des clauses par défaut paramétrables par agence, les champs du contrat étant automatiquement injectés dans le document. Seul bémol : toutes les informations (locataire, loyer, charges, dépôt, dates) sont saisies à la main dans l'assistant de création du bail — rien ne remonte du ticket de recherche de locataire, dont il ne reste aucune trace sur le contrat.
PdfTemplate.java:4-8 (adb-reports) déclare rentalContract.ftlh, civil-rc-pdf.ftlh, commercial-rc-pdf.ftlh, rendus par ReportsServiceImpl.java:275-277 ; les champs du contrat sont liés via les annotations @TemplateComponent sur RentalContract.java (adb-model) et exposés par TemplateValuesController.java (adb-contracts). La bibliothèque de clauses françaises vit dans infra/seeds/data/adb-reports/template_meta_data.json (DOCUMENT_LAYOUT_METADATA.*). Aucun champ du contrat ne référence le ticket d'origine : ressaisie intégrale.
adb-reports/src/main/java/fr/lifeconnect/adb/reports/document/PdfTemplate.java:4adb-reports/src/main/java/fr/lifeconnect/adb/reports/service/impl/ReportsServiceImpl.java:275adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/TemplateValuesController.javainfra/seeds/data/adb-reports/template_meta_data.json
Avant signature, la plateforme effectue une vérification sérieuse du bail : dates, durée, tacite reconduction, indices, loyer et charges initiaux, modalités de paiement, bénéficiaire du dépôt, assurance des colocataires. La signature elle-même reste rudimentaire : c'est un simple changement d'état avec une date, sans signature électronique — le bail est signé sur papier ou via un outil externe, puis reporté dans la plateforme. Une fois signé, le bail devient bien automatiquement éligible au quittancement : cet enchaînement-là fonctionne.
RentalContractStateMachineConfig.java (adb-contracts) enchaîne DRAFT -> FINALIZE_OFFER -> READY_TO_SIGN -> SIGN -> SIGNED ; RentalContractReadyToSignValidator.java porte le validateur pré-signature (les contrôles lieu/date de signature y sont commentés). La signature ne fait que poser la date via ContractStateServiceImpl.java:290,322 (updateSignedDateIfNotNull) ; grep -i 'yousign|docusign|electronicSignature' -> 0 hit. SIGNED est l'état filtré par la sélection du call-for-rent ('SIGNED','NOTICED','UNENTITLED' dans RentPeriodRepository.java:24).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/RentalContractStateMachineConfig.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalContract/RentalContractReadyToSignValidator.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:290
L'état des lieux d'entrée n'existe dans la plateforme que sous forme d'une date — remplie d'office avec la date de début du bail si on ne la saisit pas — et d'un PDF joint. Pas de trame EDL : pas de pièces, d'équipements, de relevés de compteurs, de photos, ni de comparaison entrée/sortie. Seuls les honoraires d'EDL sont gérés en facturation. Conséquence : en cas de litige sur le dépôt de garantie à la sortie, la comparaison des états se fait entièrement à la main sur les documents papier, et rien n'empêche d'appeler le premier loyer et le dépôt sans qu'aucun EDL n'ait été fait.
RentalContract.java:424 inventoryDateIn, :450 inventoryDateOut, :471 inventoryOutCompliance sont les seuls champs EDL ; ContractServiceImpl.java:1010 fait rc.setInventoryDateIn(rc.getBegin()) par défaut, silencieusement. Aucune entité EDL (pièces/équipements/compteurs/photos). Les natures d'honoraires RENTING_INVENTORY_*_FEES existent dans adb-utilities/*-values-input.tsv. L'EDL ne conditionne ni la remise des clés ni le premier quittancement.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:424adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractServiceImpl.java:1010
L'appel du dépôt de garantie fonctionne parfaitement : il est généré automatiquement avec le tout premier appel de loyer, pour le montant prévu au bail, comptabilisé à la date de début du contrat avec des écritures en partie double correctes. Le locataire le reçoit sur son compte en même temps que son premier avis d'échéance, sans aucune intervention manuelle.
InitialRent.java:128-140 (adb-accounting) ajoute CallForDeposit comme événement secondaire ; le montant vient de RentalContract.java:566 (CALL_FOR_DEPOSIT -> rentDeposit(rentDepositAmount)) et la date de comptabilisation est le début du contrat (:755). Événement documenté dans adb-doc/docs/concepts/accounting/events/call-for-deposit.md. Émis dans le même fan-out que le premier CALL_FOR_RENT.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InitialRent.java:128adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/CallForDeposit.javaadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:566adb-doc/docs/concepts/accounting/events/call-for-deposit.md
L'attestation d'assurance habitation (MRH) du locataire est bien enregistrée comme un fichier avec une période de validité, et la plateforme sait afficher si elle est expirée. Mais la relance annuelle automatique, pourtant développée (tâche planifiée et courriel type), est désactivée dans le code : aucun rappel ne part réellement aujourd'hui. Et rien ne bloque la remise des clés ni le quittancement en l'absence d'attestation. Le suivi des assurances repose donc entièrement sur des relances manuelles, avec le risque de locataires non assurés passés inaperçus.
NotifyAboutExpiredCertificatesJob.java:53 (adb-files) a son @Scheduled commenté ('deactivated until further details', LCS-1426), sur FILE_TYPE.INSURANCE_CERTIFICATE + FILE_NATURE.MULTI_RISKS_RESIDENCE. RentalContractTenantCertificateAggregator.java (adb-aggregates) calcule le flag isExpired avec une constante d'alerte à 6 mois, et CertificateExpiredEmailNotificationGeneratorImpl.java (adb-reports) génère le mail — jamais déclenché. Aucun blocage du quittancement sur MRH manquante.
adb-files/src/main/java/fr/lifeconnect/adb/files/job/NotifyAboutExpiredCertificatesJob.java:53adb-aggregates/src/main/java/fr/lifeconnect/adb/aggregates/RentalContractTenantCertificateAggregator.javaadb-reports/src/main/java/fr/lifeconnect/adb/reports/CertificateExpiredEmailNotificationGeneratorImpl.java
Le premier appel de loyer fonctionne bien : le gestionnaire lance la proposition puis le traitement pour la période (pratique normale en gestion locative française), et la plateforme génère automatiquement l'ensemble — loyer, provisions pour charges, TEOM, dépôt de garantie, et les honoraires de mise en location et de rédaction, comptabilisés au premier appel. Tout se propage automatiquement entre les modules. Un point reste à confirmer avec l'ingénieur : dans certains cas, un bail nouvellement signé pourrait ne pas être retenu par son tout premier appel de loyer ; à surveiller lors des premières échéances.
ContractAccountingController.java:51-105 (adb-contracts) expose propose/process par période, par requête ou par contrat, implémentés dans ContractAccountingServiceImpl.java:377-416 ; InitialRent.java:90-140 (adb-accounting) fan-out CALL_FOR_RENT, CALL_FOR_RENTAL_CHARGES, CFR_CHARGES_HOUSEHOLD_WASTE, CALL_FOR_DEPOSIT, piloté par triggers Mongo + SQS. Caveat à vérifier : la requête candidate exige nextCallForRentPeriod (RentPeriodRepository.java:24/53) alors que ContractServiceImpl.java:1099 ne l'initialise que pour les baux TAKE_OVER — l'éligibilité d'un bail neuf à son premier CFR dépend de l'initialisation de ce champ ailleurs.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/ContractAccountingController.java:51adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractAccountingServiceImpl.java:377adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InitialRent.java:90adb-doc/docs/archive/technical-epics/contracts/i-have-call-for-rent-process/i-have-call-for-rent-process.md
Distribution : l'avis d'échéance est produit par contrat mais l'envoi est mort (aucun publisher, adb-notifications non déployé, encore sur RabbitMQ, endpoint /notifications/call-for-rent absent d'adb-reports). Le moteur appelle le loyer puis s'arrête.
Chaque mois, la plateforme sait identifier seule les baux à quittancer : elle tient compte de la périodicité (mensuelle ou trimestrielle), du terme échu ou à échoir, de l'état du contrat et de sa date de fin. Le gestionnaire n'a pas de liste à constituer à la main : la sélection des contrats à appeler est fiable et automatique, et alimente directement l'étape de génération des appels de loyer.
RentPeriodRepository.java:23-88 dérive les candidats par agrégation Mongo sur nextCallForRentPeriod, filtrée sur les états SIGNED/NOTICED/UNENTITLED. ContractAccountingServiceImpl.java:490-520 (validateRentPeriod, isPeriodValidForContract) valide la période contre rentPaymentTerm et la date de fin du contrat. Périodicité mensuelle/trimestrielle et terme échu/à échoir sont couverts.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/RentPeriodRepository.java:23-88adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractAccountingServiceImpl.java:490-520
Le lancement du quittancement mensuel existe et est bien outillé : simulation à blanc avant validation, liste des contrats cochables, récapitulatif PDF et compteurs (appelés / non appelés / en erreur). En revanche, rien ne se déclenche tout seul : un gestionnaire doit lancer le traitement à la main, chaque mois et pour chaque organisation. Un oubli signifie un mois entier de loyers non appelés, sans alerte.
ContractAccountingController.java:51-116 expose /accounting/rent/periods/{period} avec les variantes /propose (dry-run sans persistance, résultats poussés en WebSocket) et /query ; l'UI est dans call-for-rents.component.html:90 et generate-rent-calls.component.html:11. Aucun déclenchement planifié : le seul job schedulé du service est ContractRenewalJobExecutor (renouvellement de contrat), et le grep du monorepo ne trouve aucun scheduler de quittancement ni orchestration multi-agences.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/ContractAccountingController.java:51-116adb-ui/src/app/content/components/accounting/call-for-rents/call-for-rents.component.html:90adb-contracts/src/main/java/fr/lifeconnect/adb/contract/schedule/impl/ContractRenewalJobExecutor.java
La génération des appels fonctionne bien : pour chaque bail, la plateforme produit en une seule passe l'appel de loyer, l'appel de charges et la TEOM, et applique au passage la révision de loyer (indexation IRL) avec son rapport. Les écritures comptables correspondantes s'enchaînent automatiquement entre les modules, sans intervention humaine.
ContractAccountingServiceImpl.java:405-450 (handleContractForCFRPeriod) fait passer la période INITIAL→PROCESSING ; côté comptabilité, ContractsFunctions.java:158-172,245-268 (onContractModified → buildRecurringRents) crée l'événement parent RECURRING_RENT qui englobe CALL_FOR_RENT, CALL_FOR_RENTAL_CHARGES et CFR_CHARGES_HOUSEHOLD_WASTE (TEOM). La chaîne est event-driven : trigger Mongo → EventBridge → SQS, conformément à l'épopée i-have-call-for-rent-process.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractAccountingServiceImpl.java:405-450adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.java:158-172adb-doc/docs/archive/technical-epics/contracts/i-have-call-for-rent-process/i-have-call-for-rent-process.md:14-17
Quand l'appel de loyer d'un bail échoue pendant le traitement (par exemple à cause d'un dépôt de garantie mal renseigné, une erreur bloquante confirmée par l'audit), le contrat est bien signalé en erreur — mais il devient alors impossible à rappeler depuis l'application : la case est grisée et le bail sort silencieusement du quittancement du mois. Le loyer n'est donc pas appelé, et la seule remise en route passe par une intervention directe en base de données par un informaticien. Concrètement : des loyers non facturés tant que personne ne s'en aperçoit.
RecurringRentAccountingEventProcessor.java:244-264 (onErrorEventState) positionne nextCallForRentPeriod.state=ERROR, mais les candidats sont filtrés sur INITIAL uniquement (ContractAccountingServiceImpl.java:490-500) et la transition d'état exige current=INITIAL (ContractUpdateRepositoryImpl.java:57-65) ; l'UI grise la ligne (generate-rent-calls.component.html:61). Un contrat ERROR n'a donc aucun chemin de reprise produit — seule issue : le script Mongo reset_accounting.js. Cause fréquente : NPE sur dépôt de garantie null qui bloque tout premier appel (F047).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/RecurringRentAccountingEventProcessor.java:244-264adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/ContractUpdateRepositoryImpl.java:57-65adb-ui/src/app/content/components/accounting/call-for-rents/generate-rent-calls/generate-rent-calls.component.html:61adb-infrastructure/mongodb-atlas/data-scripts/reset_accounting.js
L'avis d'échéance est bien produit : un PDF par bail et par période, généré automatiquement et rangé dans les documents du contrat, consultable aussi depuis la fiche locataire. Deux réserves : si la génération d'un PDF échoue, l'incident est seulement consigné dans les journaux techniques, sans liste d'exceptions pour le gestionnaire ; et le document ne part pas de lui-même vers le locataire — l'étape suivante (l'envoi) n'est pas alimentée automatiquement, l'opérateur doit ouvrir et télécharger chaque fichier.
ProcessAccountingEventStrategy.java:140-154 (attachIndividualCallForRentReport) déclenche la génération après le CFR ; ContractDocumentServiceImpl.java:117-130 enregistre le PDF dans adb-files sous fileNature CALL_FOR_RENT avec une clé unique idempotente ; le template FreeMarker est call-for-rent-individual.ftlh:358,565. Le subscribe est fire-and-forget : un échec de génération est seulement loggué, aucune liste d'exceptions n'est constituée, et rien ne route le PDF vers le locataire.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessAccountingEventStrategy.java:140-154adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractDocumentServiceImpl.java:117-130adb-charts/releases/templates/call-for-rent-individual.ftlh:358
L'envoi de l'avis d'échéance au locataire n'existe pas dans la pratique : ni courriel automatique, ni courrier, ni mise à disposition sur un espace locataire. Un modèle d'e-mail a été préparé mais la chaîne qui devait le déclencher est cassée à trois endroits distincts et n'a jamais fonctionné en production. Résultat : la diffusion de milliers d'avis d'échéance chaque mois se fait entièrement en dehors de la plateforme (envoi manuel ou autre outil), avec la charge de travail et le risque d'oubli que cela implique — alors que les relances d'impayés, elles, disposent bien de canaux e-mail et courrier.
Trois ruptures indépendantes : (1) aucun producteur du message CFR — le consommateur adb-notifications/ContractsFunctions.java:23-31 n'a zéro producteur (grep CallForRentMessageContent, onContractCallForRent, sendCallForRentEmail sur tout le monorepo) ; (2) adb-notifications est bindé RabbitMQ (application.yml:34-51), décommissionné, et absent des charts prod (pas d'adb-notifications.yaml dans adb-charts/charts/services_prod/templates/) ; (3) ReportsClientImpl.java:37 cible /notifications/call-for-rent, inexistant dans adb-reports (NotificationGeneratorController n'expose que /notifications/certificates). Le PDF lui-même est bien produit (étape 27) ; seul le canal sortant manque — le dunning a EMAIL/LETTER/LRAR, le CFR n'a rien. Template inutilisable : adb-ui/EMAIL-Template/call-for-rent.html.
adb-notifications/src/main/java/fr/lifeconnect/adb/notifications/function/ContractsFunctions.java:23-31adb-ui/EMAIL-Template/call-for-rent.html
Le prélèvement SEPA est proposé automatiquement pour chaque bail appelé, avec plafonnement au mandat. Mais la suite est fragile et manuelle : le gestionnaire doit valider le lot, télécharger le fichier bancaire et le déposer lui-même sur le portail de la banque. Surtout, le fichier généré est incomplet au regard du standard (l'audit a confirmé qu'il peut être rejeté par la banque, et la distinction premier prélèvement / prélèvement récurrent n'est pas gérée), et les rejets de prélèvement renvoyés par la banque ne sont jamais réintégrés : chaque impayé SEPA doit être repéré et traité à la main.
ProcessAccountingEventStrategy.java:173-185 (createDirectDebit) auto-propose le prélèvement après le CFR ; DirectDebitController.java:88-172 expose propose/process/process-batch et /export XML, mappé par AbstractDirectDebitXmlMapper sur les XSD pain.008.001.02/10. Bugs confirmés : le pain.008 est émis sans PmtTpInf (rejetable banque) et sans séquence FRST/RCUR ; aucune ingestion des R-transactions/rejets nulle part dans le monorepo. Traitement du lot, téléchargement et transmission banque sont des actions UI manuelles (call-for-rents.component.html:139-157).
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/strategy/accounting/impl/ProcessAccountingEventStrategy.java:173-185adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/DirectDebitController.java:88-172adb-contracts/src/main/resources/xsd/pain.008.001.02.xsdadb-ui/src/app/content/components/accounting/call-for-rents/call-for-rents.component.html:139-157
L'encaissement fonctionne en apparence : un prélèvement traité crée et comptabilise automatiquement le règlement du locataire, et les encaissements manuels (chèque, virement) se saisissent dans un écran dédié. Mais le règlement SEPA est comptabilisé dès la génération du fichier bancaire — avant même que la banque confirme — et il n'est jamais rapproché des sommes appelées : le loyer reste affiché comme dû alors que l'argent est encaissé. Sans lettrage manuel systématique, les soldes locataires sont faux et les traitements en aval (reversement au propriétaire) ne se déclenchent pas.
ContractsFunctions.java:191-212 (onDirectDebitProcessed) crée le paiement TENANT_PAYMENT_RECEIVED (idempotence via forSepa) ; DirectDebitPaymentCreationStrategy.java:52 le passe en isAutomaticBooking=true et PaymentServiceImpl.java:446-457 (asyncPaymentBooking) le comptabilise (BOOK). Bug F055 : le crédit SEPA est comptabilisé à la génération du fichier et jamais lettré contre les débits de l'appel (voir étape 31). Encaissements manuels : adb-ui accounting-recieve-payment.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.java:191-212adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/creation/impl/DirectDebitPaymentCreationStrategy.java:52adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:446-457
Le lettrage manuel est bien conçu : la plateforme propose d'elle-même une affectation du règlement sur les dettes les plus anciennes, que le gestionnaire n'a plus qu'à valider. En revanche, le lettrage automatique des prélèvements SEPA, pourtant écrit dans le code, n'est jamais déclenché : chaque encaissement par prélèvement — c'est-à-dire l'essentiel des loyers — doit être lettré à la main, un par un. À défaut, les appels restent ouverts et les traitements suivants, dont le reversement au propriétaire, ne partent jamais.
SettlementController.java:42-59 expose /settlements/{entryId}/propose|/process (proposition FIFO dette-la-plus-ancienne, validée dans recieve-payment-modal.component.ts:235-245). Le lettrage auto SEPA est du code mort : PaymentServiceImpl.handleShare (:320-335) ne lettre que si la cible du share est une Invoice, or le share SEPA cible le mandat ; getDirectDebitPaymentSettlementSource (:353-360) a zéro call site et DirectDebitPaymentSettlementCreationStrategy n'est référencée que par la factory et les tests.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/SettlementController.java:42-59adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:320-335adb-ui/src/app/content/components/accounting/payment/recieve-payment-modal/recieve-payment-modal.component.ts:235-245
La quittance de loyer n'existe pas comme document à part entière : l'avis d'échéance du mois suivant contient simplement un encart « Quittance de loyer » récapitulant les montants appelés et déjà réglés de la période précédente — et cet encart est émis même quand rien n'a été payé. Or, juridiquement, une quittance ne doit être délivrée qu'une fois le loyer intégralement réglé : la pratique actuelle expose le bailleur (document valant reconnaissance de paiement remis à tort) et il est impossible de rééditer une quittance quand un règlement tardif arrive.
Le PDF unique s'intitule « Quittance de loyer et avis d'échéance » (call-for-rent-individual.ftlh:358-359,719,812). DocumentDataFactoryImpl.java:91-141 et 339-395 construit previousPeriodReceipts depuis les écritures de la période précédente sans aucune condition de paiement. Grep sur QUITTANCE et les file natures de type receipt : seul fileNature('CALL_FOR_RENT') existe (ContractDocumentServiceImpl.java:40) — pas d'objet quittance, pas de gate paiement, pas de réédition.
adb-charts/releases/templates/call-for-rent-individual.ftlh:358-359adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DocumentDataFactoryImpl.java:91-141adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractDocumentServiceImpl.java:40
La détection des impayés est automatique : dès que le solde du locataire évolue, la plateforme alimente une liste de relances avec niveaux d'escalade et courriers PDF. Le problème est grave côté envoi : l'envoi par e-mail a été désactivé dans le code, mais la relance est tout de même enregistrée comme envoyée (constat critique de l'audit). Les gestionnaires croient donc que les locataires ont été relancés alors que rien n'est parti ; les canaux courrier et LRAR ne font que stocker le fichier, l'expédition réelle restant entièrement à la charge de l'équipe. Conséquence directe : des impayés qui s'installent et une trace de relance juridiquement trompeuse.
DunningServiceImpl.java:219-247 (processBalanceEvent) consomme la queue auxiliary-balance-updated et crée les Dunning ; DunningController.java:33-40 expose /contracts/dunnings/notices et DunningServiceImpl.java:251-322 génère le PDF de relance avec ses métadonnées fichier. Bug F084 (critique, constat C2) : sendNoticeEmail (DunningServiceImpl.java:529-541) a son corps commenté et retourne Mono.empty(), tandis que la relance est quand même enregistrée comme envoyée ; LETTER/LRAR sont du pur stockage de fichier.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:529-541adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/DunningController.java:33-40adb-ui/src/app/content/components/dunning/
La livraison : adb-contracts génère le PDF de relance et incrémente le niveau, mais l'envoi email est commenté (DunningServiceImpl.java:539-541) et aucun prestataire courrier/LRAR n'existe ; toute la suite suppose une relance délivrée.
La plateforme tient bien une liste des locataires débiteurs, mise à jour automatiquement à chaque mouvement de compte. Mais le seul critère est « solde positif » : un locataire y apparaît dès l'émission de l'appel de loyer, avant même la date d'échéance, sans seuil de montant, sans ancienneté de la dette, sans règle par mandat. Le gestionnaire doit donc trier lui-même les vrais impayés des simples appels en cours, et rien ne déclenche de relance tant qu'il n'ouvre pas l'écran pour cocher des lignes à la main.
La liste de candidats est événementielle : DunningFunction.java:34-47 consomme contractsAuxiliaryAccountBalanceUpdated et DunningServiceImpl.processBalanceEvent (DunningServiceImpl.java:220-247) rafraîchit l'entité. Mais DunningRepositoryImpl.findAll (DunningRepositoryImpl.java:161-169) filtre uniquement versions.idx=1 AND balance.amount > 0 ; un grep dueDate/overdue/echeance/threshold sur adb-contracts et le modèle dunning ne retourne rien : pas de buckets d'ancienneté, pas de montant minimal, pas de politique par mandat. Rien ne sélectionne ni ne déclenche une relance : l'opérateur passe par /lists/dunning.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:220-247adb-contracts/src/main/java/fr/lifeconnect/adb/contract/function/DunningFunction.java:34-47adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:161-169
La lettre de relance simple existe et est de bonne facture : en-tête agence, locataire, contrat, extrait de compte et total dû. Deux problèmes sérieux : le niveau de relance est seulement proposé à l'écran, sans aucun délai imposé — on peut envoyer la relance 1 et la relance 2 le même jour — et surtout le courrier facture les frais de relance au locataire, une clause illégale au regard de la loi de 1989 qui expose l'agence à un risque juridique sur chaque courrier émis. Le passage au niveau suivant ne se fait par ailleurs que si un gestionnaire revient sur l'écran et resélectionne le dossier.
Le rendu L1 passe par le template dunning-level1.ftlh:523-562 sélectionné par le switch de DunningServiceImpl.java:391-408. Le niveau est proposé côté client par proposeDunningLevel (dunning.mapper.ts:54-65) à partir du seul currentLevel, sans règle de délai, et reste modifiable par l'opérateur ; aucun timer ni scheduler ne réarme l'escalade. Bug confirmé F087 : le courrier L1 facture les frais de relance au locataire, clause illégale (art. 4, loi 89-462).
adb-charts/releases/templates/dunning-level1.ftlh:523-562adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:391-408adb-ui/src/app/core/mappings/dunning.mapper.ts:54-65
La relance de niveau 2, qui menace de mettre en jeu la clause résolutoire, existe et fonctionne sur le même moteur que la relance 1 ; seul le modèle de courrier change, et le niveau atteint est bien mémorisé par locataire. En revanche aucun délai minimal n'est imposé entre les deux niveaux et l'escalade n'est jamais automatique : c'est au gestionnaire de repérer que le locataire n'a pas réglé et de revenir déclencher la relance suivante à la main.
Le template dunning-level2.ftlh:528-566 annonce la résiliation de plein droit ; le niveau est proposé par dunning.mapper.ts:57-59 et stocké sur DunningEntity.currentLevel, avec previousDunningNoticeDate horodaté par locataire dans DunningRepositoryImpl.java:405-425. Aucun délai minimal entre L1 et L2 n'est vérifié nulle part ; l'escalade est une re-sélection humaine de la ligne.
adb-charts/releases/templates/dunning-level2.ftlh:528-566adb-ui/src/app/core/mappings/dunning.mapper.ts:57-59adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/impl/DunningRepositoryImpl.java:405-425
Le niveau 3 fait office de mise en demeure et coche automatiquement l'envoi en recommandé, mais ce n'est qu'un courrier : pas de suivi de l'accusé de réception, pas de numéro de recommandé, pas de date certaine, pas de délai calculé. Pire, le nom du bailleur y est figé en dur sur une seule société, ce qui rend le courrier faux pour tous les autres bailleurs du portefeuille. Et la chaîne s'arrête là : le niveau 3 peut être réémis indéfiniment, sans qu'aucune étape juridique suivante ne puisse être enregistrée dans l'outil.
Le template dunning-level3.ftlh:523-569 (« Dernière relance - dette locative », visa loi 89-462 art. 7) est le niveau terminal : DunningLevel.java ne compte que 3 valeurs, donc L3 est réémissible à l'infini et rien ne modélise l'acte suivant. sendLrar n'est auto-coché qu'à LEVEL_3 (dunning.mapper.ts:60-62,77) sans aucun suivi AR, numéro ou délai. Bug confirmé F088 : le bailleur « Société V FUND IMMO CORE » est codé en dur dans le template.
adb-charts/releases/templates/dunning-level3.ftlh:523-569adb-ui/src/app/core/mappings/dunning.mapper.ts:60-77adb-contracts/src/main/java/fr/lifeconnect/adb/contract/model/dunning/DunningLevel.java
C'est la faille la plus grave du processus : les relances ne partent pas. Pour le canal e-mail, le document est généré et archivé mais l'envoi lui-même est désactivé dans le code — le locataire ne reçoit rien alors que le dossier indique le contraire. Pour le courrier simple et le recommandé, la plateforme ne produit qu'un PDF que le gestionnaire doit imprimer et poster lui-même, sans preuve de dépôt ni suivi de distribution. L'historique affirme donc des envois qui n'ont jamais eu lieu, ce qui fragilise toute procédure ultérieure (mise en demeure, contentieux, dossier assureur).
Bug confirmé F084 : dans DunningServiceImpl.sendNoticeEmail (DunningServiceImpl.java:530-542), l'appel emailService.send(mailingInfo) est commenté et la méthode retourne Mono.empty(), avec sujet « pay » et corps "<html>Content</html>" codés en dur — le niveau s'incrémente et la relance est marquée envoyée sans qu'aucun mail ne parte. Les canaux LETTER/LRAR (DunningServiceImpl.java:298-322) se contentent d'archiver le PDF dans adb-files ; grep maileva/courrier/postal/recommandé sur tous les modules : aucun prestataire postal, aucun champ de statut de distribution.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:530-542adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:298-322
L'information de la caution n'existe tout simplement pas dans la plateforme. Un acte de cautionnement peut être rattaché au bail, mais le circuit de relance l'ignore totalement : impossible de produire le courrier d'information à la caution, de l'enregistrer, ou même de lister les dossiers en relance qui disposent d'un garant. Tout se fait hors outil — et une caution non informée dans les délais peut être déchargée des pénalités de retard, c'est-à-dire une garantie perdue en silence.
Grep guarantor/garant/caution sur adb-contracts/src/main et adb/adb-model : seuls GuaranteeContractController/GuaranteeContractServiceImpl et ContactCategory.GUARANTOR (côté signature) existent. Aucun champ garant sur Dunning.java, DunningNotice.java, TenantEntity ni DunningDocumentModel.java ; DunningDocumentServiceImpl.composeDocumentModel ne charge que l'agence, le locataire et les écritures comptables. La fonctionnalité vivrait naturellement dans le flux dunning d'adb-contracts.
Le commandement de payer est partiellement outillé : un huissier peut être référencé comme intervenant typé dans un dossier contentieux, et la refacturation de ses frais au locataire passe par une écriture typée « Commandement de payer ». Mais l'acte lui-même n'existe pas dans l'outil : pas de date de signification, pas de décompte du délai de deux mois de la clause résolutoire, pas de document type, et ni le dossier de relance ni le bail ne changent d'état. Le gestionnaire saisit une écriture comptable à la main et suit les échéances légales de son côté.
L'huissier est un tiers typé (CATALOG.CPY_ACT.BAILIFF) rattachable via PATCH /tickets/litigations/{id}/suppliers (TicketController.java:530) dans un dossier contentieux, et la refacturation passe par l'événement SUMMONS_TO_PAY_DUE_BY_TENANT (CATALOG_fr.properties:40, compte 706645 coa.csv:92, saisi via entry-details.component.ts:82, flagué Manual=yes ; idem LEGAL_COSTS_DUE_BY_TENANT). Mais aucun objet acte, aucune date de signification, aucun décompte des deux mois, et ni Dunning ni RentalContract.java:1242 ne sont mis à jour : le dossier de relance n'apprend jamais qu'un commandement a été signifié.
adb-accounting/src/main/resources/CATALOG_fr.properties:40adb-accounting/src/main/resources/coa.csv:92adb-ui/src/app/content/pages/accounting/accounting-manual-entries/entry-details/entry-details.component.ts:82adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1242
Un embryon de suivi contentieux existe : un dossier « Contentieux » (impayé ou squat) peut être ouvert, avec ses étapes, ses intervenants, ses pièces et ses notes, consultable depuis la fiche du bail. Mais la procédure judiciaire elle-même n'est pas modélisée : pas d'assignation, pas de date d'audience, pas de jugement, pas de protocole d'accord ni de suivi d'expulsion, et rien ne relie automatiquement les relances au dossier contentieux. Un bail résilié par la clause résolutoire est enregistré comme une simple fin de bail : impossible de distinguer, dans l'outil, un départ amiable d'une expulsion.
adb-tickets fournit un type LITIGATION (TICKET_TYPE.LITIGATION=Contentieux, natures UNPAID/SQUAT) avec machine à états (TicketState.availableTransitions, LegalTransition), LitigationTicketDetails (montants payés/impayés, squatter), intervenants, pièces et notes, plus les écrans adb-ui litigations-page, litigation-details-page et rc-tickets-tab. Mais aucun champ audience/jugement/expulsion, aucun handoff dunning→ticket, et ContractState.java n'a pas d'état contentieux ni résiliation-judiciaire : grep assignation/clause resolutoire/expulsion/tribunal sur tous les modules Java et adb-ui ne touche que la prose de dunning-level2.ftlh:539 et dunning-level3.ftlh:533 ; TERMINATED reste l'état générique sans cause.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/ContractState.javaadb-charts/releases/templates/dunning-level2.ftlh:539adb-charts/releases/templates/dunning-level3.ftlh:533
La plateforme sait qu'un bail est couvert par une assurance loyers impayés (GLI) et l'affiche dans la liste des relances, et l'encaissement de l'indemnité en aval est bien automatisé. Mais la déclaration du sinistre elle-même n'existe pas : pas de dossier de sinistre impayé, pas de délai de déclaration surveillé, pas de contrôle de franchise ou de plafond, pas de statut de la demande. Le dossier de preuves que l'assureur exige — l'historique des relances — n'est jamais assemblé : le gestionnaire ressaisit tout sur l'extranet de l'assureur, avec le risque de rater un délai de déclaration et de perdre la garantie.
Grep sinistre/claim/declaration sur adb-ui/src/app et adb-contracts : aucun résultat côté GLI-impayés ; InsuranceContract.java, InsuranceOption.java et InsuranceRate.java ne modélisent que primes/taux/options, et les écrans adb-ui (create-gli, gli-list, display-gli-contract, AttachGliToRC) ne couvrent que la souscription. Côté relance, le GLI se résume au booléen Dunning.activeGli posé par DunningServiceImpl.java:346-367 depuis un contrat d'assurance SIGNED lié. Il existe bien un type de ticket DAMAGE « Sinistres » (TicketDetails.insuranceIndemnity, insuranceInCharge) et la chaîne de règlement typée GLI_PAYMENT_RECEIVED/GLI_PAYMENT_PAYBACK/GLI_TO_OWNER, mais aucun objet déclaration ni transmission du dossier de relances.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/DunningServiceImpl.java:346-367adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/InsuranceContract.java
C'est l'étape la mieux outillée de la chaîne : quand l'indemnité de l'assureur arrive, le gestionnaire enregistre un encaissement avec le mode « GLI (indemnités reçues) » et la comptabilité s'écrit toute seule — constatation de l'indemnité, bascule de la créance du locataire en créance douteuse, et lettrage automatique des loyers impayés. Un bémol en aval : le reversement de cette indemnité au propriétaire ne s'enchaîne pas depuis ce chemin (voir l'étape suivante).
AbstractPaymentStrategy.java:52-57 mappe PAYMENT_METHOD.GLI vers InsurancePaymentReceived ; InsurancePaymentReceived.java:54-77 déclare comme friends InsuranceIndemnitiesReceived et TenantToBadDebtEvent, et PostingRuleFactory.java:84-116 poste 512101 D / 405100 C, le lettrage soldant les écritures de loyer ouvertes du locataire. La jambe créditant le propriétaire reste inatteignable depuis ce chemin (voir étape 44 / F076).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/payment/processing/impl/AbstractPaymentStrategy.java:52-57adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InsurancePaymentReceived.java:54-77adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:84-116
L'écriture qui reverse l'indemnité GLI au propriétaire est bien codée, mais elle est inatteignable par le circuit normal : la condition qui la déclenche n'est jamais remplie quand l'indemnité est encaissée par l'écran prévu à cet effet. Conséquence directe pour le propriétaire : l'agence encaisse l'indemnité mais le compte du propriétaire n'est jamais crédité, et son relevé de gérance (CRG) ne montre rien à lui reverser. Seule une écriture manuelle d'une nature bien précise permet de contourner le blocage — de l'argent dû aux propriétaires peut donc rester chez l'agence sans que rien ne l'alerte.
Bug confirmé F076 (critique) : GliToOwner (800000 D / 466100 C) est codé mais inatteignable. PostSettlementsProcessorImpl.java:49-53 limite ALLOWED_CREDIT_EVENT_TYPE à SUPPLIER_PAYMENT_ISSUED, TENANT_PAYMENT_RECEIVED et MANUAL_ENTRY (contrôle :201-206), et ManualEntryEvent.java:162-176 exige CREDIT_EVENT_TYPE == GLI_INDEMNITIES_RECEIVED ; or PaymentServiceImpl.java:570 lettre sous settlingAccountingEventType = GLI_PAYMENT_RECEIVED, type non éligible — le post-settlement ne se déclenche jamais. F076 note aussi que le journal correspondant est absent du seed de prod. Contournement : booker l'indemnité en MANUAL_ENTRY de nature GLI. Voir adb-doc/docs/concepts/accounting/engine-audit-2026-07.md:57,157.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:49-53adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/ManualEntryEvent.java:162-176adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PaymentServiceImpl.java:570adb-doc/docs/concepts/accounting/engine-audit-2026-07.md:57
L'esprit de la subrogation est respecté : dès que l'indemnité est comptabilisée, la dette du locataire bascule automatiquement en « Locataires douteux », donc la créance reste bien ouverte. Mais ce compte est unique pour toute l'agence, sans détail par locataire : tous les dossiers indemnisés par la GLI y sont fondus ensemble, et il est impossible de lire dans la comptabilité qui doit encore combien. Le suivi du recouvrement locataire par locataire se fait donc hors comptabilité, sur tableur.
TenantToBadDebtEvent.java déclenche via PostingRuleFactory.java:118-148 le posting tenantBadDebts 416100 D / analytique 800000 C. Mais 416100 « Locataires douteux » est un compte GL au niveau organisation (LedgerAccountShort.java:249, coa.csv:32) sans auxiliaire par locataire, contrairement à 411100/RC : tous les locataires « GLIsés » sont poolés et le recouvrement individuel n'est pas réconciliable depuis le grand livre.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/TenantToBadDebtEvent.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:118-148adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/LedgerAccountShort.java:249adb-accounting/src/main/resources/coa.csv:32
Un écran dédié permet d'enregistrer le remboursement du locataire, et le cas « le locataire rembourse la GLI à l'agence » fonctionne bien : encaissement, apurement du compte douteux et reversement au propriétaire s'enchaînent correctement. Mais l'écran propose trois natures de remboursement, et les deux autres — remboursement à l'assureur et remboursement au propriétaire — n'écrivent rigoureusement rien en comptabilité, sans aucun message d'alerte. Le gestionnaire doit repérer lui-même ces enregistrements muets et repasser les écritures à la main.
L'écran gli-tenant-payback.component.ts (avec proposeGLIPayback) propose les trois natures de CATALOG_fr.properties:44-46, mais InsurancePaybackPaymentEvent.friends() (InsurancePaybackPaymentEvent.java:63-100) n'est renseigné que pour AGENCY_COMPENSATION — chaîne PaymentEvent + TenantToBadDebtClear (crédit 416100) + TenantToOwner. Les deux autres natures retournent une liste friends() vide et ne comptabilisent rien, silencieusement.
adb-ui/src/app/content/pages/accounting/gli-tenant-payback/gli-tenant-payback.component.tsadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InsurancePaybackPaymentEvent.java:63-100adb-accounting/src/main/resources/CATALOG_fr.properties:44-46
Rembourser l'assureur (trop-perçu ou sommes recouvrées auprès du locataire) reste une opération entièrement manuelle : l'écriture documentée est marquée « manuelle » et ne se génère pas, la voie qui fonctionne étant de saisir une facture fournisseur au nom de l'assureur. Surtout, rien ne rapproche ce que le locataire a remboursé de ce qui doit repartir vers l'assureur : c'est au gestionnaire de calculer et de saisir le montant, avec un risque d'oubli ou d'erreur au détriment de l'assureur ou de l'agence.
InsurancePaybackPaymentEvent porte postingRuleGenerator = FALSE : l'événement porteur ne génère aucune règle de posting, et le schéma documenté 405100 D / 512101 C est flagué Manual=yes dans adb-doc/docs/concepts/accounting/events/gli-payment-payback.md. La route opérationnelle est InsurancePaybackInvoiceCreationStrategy.java (facture fournisseur vers l'assureur). Rien ne dérive le montant dû des remboursements locataire ou des indemnités reçues.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/InsurancePaybackPaymentEvent.javaadb-doc/docs/concepts/accounting/events/gli-payment-payback.mdadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/InsurancePaybackInvoiceCreationStrategy.java
La sortie du processus n'existe pas vraiment : le plan comptable prévoit bien un compte de perte pour créances irrécouvrables et le passage en créances douteuses est automatique, mais aucune opération de l'outil ne constate jamais la perte définitive — pas de passage en perte typé, pas de provision. Pour solder un dossier définitivement irrécouvrable, le gestionnaire doit passer une écriture manuelle libre. Faute de quoi, le compte des locataires douteux enfle indéfiniment, sans détail ni porte de sortie, et l'encours réel d'impayés devient illisible.
coa.csv:80 contient 671400 « Créances irrécouvrables » (LOSS_BAD_DEBT, documenté dans plan-of-accounts.md:81) et les événements typés TENANT_TO_BAD_DEBT / TENANT_TO_BAD_DEBT_CLEAR postent automatiquement 416100↔800000 — mais uniquement depuis une indemnité GLI, et rien ne poste jamais 671400. Grep 654/irrecouvrable/WRITE_OFF/writeOff sur adb-accounting/src/main, adb/adb-model et coa.csv : aucun compte 654xxx ni 491 (provision), aucune constante de write-off dans AccountingEventType.java. La sortie réelle reste une MANUAL_ENTRY libre.
adb-accounting/src/main/resources/coa.csv:80adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java
Étape 4 : aucune campagne. La paire propose/process existe dans l'API d'adb-contracts mais sans écran ; la production révise des milliers de baux depuis un message SQS — sans prévisualisation, comptages, exceptions ni rollback.
Chaque trimestre, la plateforme récupère automatiquement les indices INSEE (IRL, ICC, ILAT, ILC) dès leur publication, avec la date officielle du Journal Officiel, et un rafraîchissement manuel reste possible. Cette étape fonctionne correctement et déclenche automatiquement la suite du processus de révision. Seule limite : si l'INSEE corrige un indice déjà publié, la correction est bien enregistrée mais ne déclenche aucune nouvelle révision des loyers concernés.
InseeIndexJob (adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/index/job/InseeIndexJob.java:48) est un cron Europe/Paris conditionné par SYSTEM_CONFIGS.INSEE_PUBLICATION_DATE ; IndexServiceImpl.syncInseeIndexes (index/service/impl/IndexServiceImpl.java:151) upserte les quatre séries IRL/ICC/ILAT/ILC avec trimestre de validité et date JO, et IndexController.java:33 expose un resync manuel. Faille du handoff : le trigger Atlas index-created.json ne s'abonne qu'aux INSERT alors que le service fait un updateOrInsertInseeIndexes — une valeur INSEE révisée (UPDATE d'un trimestre existant) n'émet aucun message et ne re-révise rien.
adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/index/job/InseeIndexJob.java:48adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/index/service/impl/IndexServiceImpl.java:151adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/index/controller/IndexController.java:33adb-infrastructure/mongodb-atlas/realm-cli/triggers/script/configuration/utilities/index-created.json
La plateforme sait repérer les baux dont la date anniversaire de révision correspond à l'indice qui vient d'être publié, pour quatre catégories de baux. Mais la sélection est incomplète : la périodicité triennale des baux commerciaux est ignorée, l'encadrement des loyers (loyer de référence majoré) n'existe nulle part dans l'outil, et la liste des baux retenus n'est jamais montrée au gestionnaire — impossible de vérifier qui a été inclus ou exclu avant que la révision ne parte. En zone tendue, un dépassement du plafond d'encadrement doit donc être contrôlé à la main, avec un risque juridique en cas d'oubli.
La sélection est ContractRepository.findContractCandidatesByIndexKey (adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/ContractRepository.java:142-152) : match sur nextExpectedIndexKey (type+année+trimestre) pour les contrats RENTAL de 4 catégories (IndexServiceImpl.java:60-66), sans filtre organisationId ni état de contrat. La fréquence d'indice (triennale commerciale) n'est pas prise en compte ; grep encadrement|rentControl|referenceRent|loyer de référence sur adb, adb-contracts, adb-ui/src, adb-parts, adb-doc : aucun hit. Le gel DPE (adb/adb-model/.../rule/impl/energy/) est appliqué plus tard par contrat, pas en filtre de sélection ; le candidate set est un Flux réactif consommé dans le même appel, jamais matérialisé en liste consultable.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/repo/ContractRepository.java:142-152adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:60-66
Le calcul du nouveau loyer révisé est solide et conforme : application du rapport entre l'ancien et le nouvel indice, gel Climat et Résilience pour les logements classés F et G, plafonnement lié au bouclier sur le pouvoir d'achat, et distinction loyer/charges selon le type de forfait. C'est un vrai moteur de règles qui produit un montant tracé avec son statut (réussi, avertissement ou erreur). Seul bémol : si le calcul échoue pour un bail donné, celui-ci reste simplement à l'ancien loyer sans alerte visible pour le gestionnaire.
Chaîne complète dans RuleFactory (adb/adb-model/.../contract/rule/RuleFactory.java:22-37) : IndexationEligibilityAction.java:20-30 (loyer toujours, charges si LUMPSUM), EnergyClassRule + BadEnergyClassAssessor (gel F/G), ForceIndexationAction.java:28-41, RentAmountAction.java:38-56 (ratio newIndex/currentIndex), PurchasePowerAssessor (plafonnement), avec fallback ERROR ; produit un EffectiveRentAmount nature INDEXATION, statut SUCCESS/WARNING/ERROR, appliqué via RentalContract.java:1493-1546. Les échecs par contrat sont avalés : IndexServiceImpl.java:252 .onErrorContinue(log.warn) et RentAmountServiceImpl.java:201 .onErrorReturn(rc) — le bail reste non révisé avec une simple ligne de log.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/rule/RuleFactory.java:22-37adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1493-1546adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:252
Techniquement, la plateforme sait simuler une campagne de révision (calcul sans enregistrement) puis l'appliquer — mais aucun écran ne permet au gestionnaire d'utiliser cette simulation. En production, la révision s'applique directement et automatiquement à tous les baux concernés, toutes agences confondues, sans aperçu préalable, sans compteurs, sans rapport d'exceptions et sans possibilité d'annuler un lot appliqué par erreur. Concrètement, si une campagne part avec un mauvais paramètre, il faut reprendre chaque bail un par un à la main.
Paire dry-run/commit réelle au niveau API : ContractController.java:494-587 (forceIndexation/propose|process, future/propose|process) et IndexServiceImpl.java:390-440 (ProposeOnIndexPublishedStrategy renvoie une entité non persistée vs ProcessOnIndexPublishedStrategy qui écrit). Mais IndexFunction.java:46-48 (handler SQS) appelle directement PROCESS pour tous les baux matchés, cross-organisation, et grep forceIndexation|Indexation dans adb-ui/src et adb-web : zéro hit. Pas d'entité campagne, pas de run id, pas de rollback ; propose et process sont deux appels REST indépendants sans identité partagée.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/controller/ContractController.java:494-587adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:390-440adb-contracts/src/main/java/fr/lifeconnect/adb/contract/function/IndexFunction.java:46-48
Un courrier de révision individuel par locataire (ancien et nouvel indice, ancien et nouveau loyer) est bien généré en PDF et rangé dans le dossier du bail — mais seulement au moment de l'appel de loyer, donc après que le nouveau loyer est déjà appliqué, et il n'est jamais envoyé automatiquement. Le gestionnaire doit retrouver chaque PDF, l'imprimer ou l'envoyer par mail lui-même, sans suivi ni preuve de notification. En cas de contestation, la preuve que le locataire a été informé n'existe que si ce travail manuel a été fait.
ContractTemplate.java:53-67 mappe RENT_REVIEW sur rent-review-individual-pdf.ftlh ; ContractDocumentServiceImpl.createAndSaveIndividualRentReviewRecap (ContractDocumentServiceImpl.java:76-113, fileNature RENT_REVISION) est appelé via ProcessAccountingEventStrategy.java:155-170 et RecurringRentAccountingEventProcessor.java:142-143 — uniquement pendant le traitement de l'appel de loyer où l'indice bascule, et seulement en stratégie PROCESS. Aucun envoi : seul DunningServiceImpl utilise EmailService. Données du document : DocumentDataFactoryImpl.java:151-195 ; cf. adb-doc/docs/archive/functional-epics/process/review/rent-amount-review-RC-residential-creation.md étapes 4-5.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/document/ContractTemplate.java:53-67adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/document/ContractDocumentServiceImpl.java:76-113adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/processor/accounting/impl/RecurringRentAccountingEventProcessor.java:142-143adb-doc/docs/archive/functional-epics/process/review/rent-amount-review-RC-residential-creation.md
Le passage au nouveau loyer est bien automatisé : le loyer révisé devient effectif à la date anniversaire du bail, l'historique des indices est conservé, la prochaine échéance de révision est calée un an plus tard, et un changement en cours de mois est correctement découpé. Point de vigilance juridique : la révision s'applique toujours rétroactivement à la date anniversaire, que le locataire ait été prévenu ou non ; la règle légale selon laquelle une révision demandée tardivement ne vaut que pour l'avenir (article 17-1) n'est pas prise en compte par l'outil.
RentalContract.handleStateTransition (adb/adb-model/.../contract/RentalContract.java:1106-1113) bascule l'indice futur en courant quand la période de facturation atteint whenEffective, historise une IndexTransition (:1323-1360) et avance clé/date attendues d'un an (:1598-1610) ; l'effet en cours de mois découpe la période. Limites : IndexServiceImpl.buildNewRentalIndex (adb-contracts/.../IndexServiceImpl.java:300-320) fixe whenEffective = nextExpectedRentReviewDate, stampe whenNotified = LocalDate.now() sans contrôle de notification, et met effective(true) inconditionnellement — le gate documenté de confirmation locataire et l'art. 17-1 (« vaut à compter de la demande ») ne sont pas modélisés.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1106-1113adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1323-1360adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/IndexServiceImpl.java:300-320
L'appel de loyer suivant utilise correctement le loyer révisé : le mois est découpé entre ancien et nouveau loyer si nécessaire, la ligne est signalée « révision de loyer » dans l'écran de quittancement (avec filtre dédié) et le courrier de révision y est joint. Cet écran, avec sa phase de proposition avant validation, est le seul endroit où le gestionnaire voit la révision avant qu'elle ne soit facturée — et il fonctionne bien.
RecurringRentAccountingEventProcessor.java:110-146 récupère l'EffectiveRentAmount indexé et attache le PDF de révision ; RentalContract.java:795-813 découpe la période sur le loyer futur et :832 expose isRentReviewHappened ; côté UI, generate-rent-calls.component.html:382-420 (adb-ui) affiche la colonne filtrable « Révision de loyer » (valeur NEW). Enchaînement automatique, pas de rupture constatée.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/processor/accounting/impl/RecurringRentAccountingEventProcessor.java:110-146adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:795-813adb-ui/src/app/content/components/accounting/generate-rent-calls/generate-rent-calls.component.html:382-420
L'écran de campagne des appels de loyer donne déjà une vue portefeuille utile : colonne et filtre « Révision de loyer », colonne d'état (réussi / non appelé / erreur) avec compteurs, et une erreur de révision bloque bien la période concernée au lieu de passer inaperçue. En revanche, il n'existe ni rapport d'anomalies exportable, ni filtre « baux non révisés », ni motif expliquant pourquoi un loyer n'a pas bougé (gel DPE, plafonnement, avertissement). Le gestionnaire doit donc ouvrir bail par bail pour comprendre les cas particuliers — avec le risque de laisser filer le délai d'un an au-delà duquel la révision est perdue.
Après vérification, l'écran adb-ui/.../generate-rent-calls.component.html est bien portefeuille : colonne+filtre « Révision de loyer » (rentReviewComing, RentPeriodRecap.java:213 depuis indexTransitionHistory) et colonne État SUCCESS/NOT_CALLED/ERROR adossée à l'agrégation countError/countNotCalled (RentPeriodRepository.java:106) ; l'ERROR n'est pas silencieux (PostingRuleHandler.java:63 throw sur rent-amount ERROR, AccountingEventServiceImpl.java:474 event ERROR+message, RecurringRentAccountingEventProcessor.java:183/257 période CFR en ERROR). Manquent : rapport/export d'anomalies dédié (aucun type dans ReportsServiceImpl ni templates adb-reports), filtre inverse « non révisés », codes motif (gel DPE, plafonnement), et le sous-état WARNING (grep rentAmountStatus|subState dans adb-ui : aucun hit) ; seul signal par contrat : rc-dates-bloc.component.html:284-306 (bandeau rouge « diagnostics non appliqués »).
adb-ui/src/app/content/components/accounting/generate-rent-calls/generate-rent-calls.component.htmladb-ui/src/app/content/components/contract/rc-dates-bloc/rc-dates-bloc.component.html:284-306
Le pont adb-parts/adb-utilities vers adb-accounting : réels récupérables et tantièmes existent mais jamais convertis en montants par lot/locataire ; adb-accounting/src/main ne référence ni Budget ni AllocationKey — calcul réel vs provisions hors plateforme.
Cette étape consiste à saisir au fil de l'année les factures fournisseurs (entretien, eau, TEOM, contrats de maintenance...) qui serviront de base à la régularisation des charges. La plateforme fait bien ce travail : chaque facture est enregistrée avec ses lignes, son compte de charge, sa TVA, son fournisseur et son circuit de validation, puis comptabilisée. C'est le maillon solide de la chaîne, et la part récupérable se renseigne sur le même écran, sans double saisie.
La capture des factures entrantes est complète : entité Invoice avec cycle de vie piloté par InvoiceStateMachine (adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/InvoiceStateMachine.java), lignes InvoiceItem portant compte de charge, TVA, fournisseur/destinataire (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.java), et saisie UI dans invoice-details-entry.component.ts. La part récupérable est saisie sur la même ligne de facture, donc pas de rupture à ce maillon.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Invoice.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/InvoiceStateMachine.javaadb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.javaadb-ui/src/app/content/components/accounting/invoices-content/invoice-details-entry/invoice-details-entry.component.ts
Cette étape doit classer chaque dépense en récupérable ou non récupérable sur le locataire, selon la liste officielle du décret 87-713. Aujourd'hui la plateforme propose seulement un pourcentage tapé librement par l'opérateur sur la ligne de facture, et une simple case à cocher côté patrimoine — deux informations qui ne se parlent pas, sans nomenclature du décret ni valeur par défaut ni contrôle de cohérence. Conséquence : la conformité au décret repose entièrement sur la vigilance du gestionnaire, avec un risque de récupérer des charges non récupérables (contestation du locataire) ou d'oublier des charges récupérables (perte pour le bailleur). De plus, le pourcentage saisi n'est réutilisé nulle part dans les calculs : l'étape suivante ne reçoit pas ce classement automatiquement.
Deux représentations non connectées : InvoiceItem.apportionAmount (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.java:69), alimenté par le champ recuperPrecent saisi librement dans invoice-details-entry.component.ts:135, et le booléen isRecoverable sur Charge (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/charges/Charge.java:39) plus le flag recoverable de BudgetLine (adb-parts/src/main/java/fr/lifecon nect/adb/part/model/domain/budget/BudgetLine.java). Grep de decret/87-713/récupérable/recoverable sur tous les modules : aucune table de règles ni nomenclature. apportionAmount est stocké et projeté mais jamais consommé par une logique d'allocation, de budget ou de régularisation — champ d'affichage uniquement.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.java:69adb-ui/src/app/content/components/accounting/invoices-content/invoice-details-entry/invoice-details-entry.component.ts:135adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/charges/Charge.java:39adb-parts/src/main/java/fr/lifeconnect/adb/part/model/domain/budget/BudgetLine.java
Cette étape doit répartir chaque dépense d'immeuble entre les lots selon les tantièmes ou clés de répartition. Un vrai moteur de clés existe dans la plateforme (les quotes-parts doivent totaliser le bon montant pour que la clé soit activée), mais il n'est en pratique jamais utilisé : la facture sait seulement désigner un lot ou une clé, la répartition elle-même n'est jamais calculée ni enregistrée. Résultat : aucun montant de charges par lot n'existe dans le système, et le gestionnaire doit refaire toute la ventilation par tantièmes à la main, généralement dans un tableur. L'étape suivante ne reçoit donc rien automatiquement.
Le moteur existe : AllocationKeyServiceImpl.applyAllocationKey et calculateAllocationKeys (adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/allocationkeys/service/impl/AllocationKeyServiceImpl.java:118) font un vrai pro-rata shares/total, exposé via POST /utilities/allocationkey/{keyId} et calculateAllocationKeyAmount côté UI (adb-ui/src/app/core/services/units/allocation-keys.service.ts:62). Mais il est mort : grep adb-ui = zéro site d'appel de calculateAllocationKeyAmount, grep AllocationKey dans adb-accounting/src/main = zéro occurrence. InvoiceItem ne stocke qu'un TargetObject d'allocation (Unit ou AllocationKey) ; aucune répartition par lot n'est calculée ni persistée, donc rien en aval ne peut agréger les charges par lot ou par locataire.
adb-utilities/src/main/java/fr/lifeconnect/adb/utilities/allocationkeys/service/impl/AllocationKeyServiceImpl.java:118adb-ui/src/app/core/services/units/allocation-keys.service.ts:62adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/InvoiceItem.java
C'est le cœur de la régularisation : comparer, pour chaque locataire, les provisions pour charges appelées pendant l'année aux dépenses réelles, au prorata de sa période d'occupation. La plateforme détient bien les deux côtés — les provisions sur le bail, les dépenses réelles et prévues sur les lignes de budget — mais aucun calcul ne les rapproche jamais, et le prorata d'occupation n'existe que pour le loyer, pas pour les charges. Le gestionnaire doit donc exporter les deux jeux de données et calculer le solde de chaque locataire dans un tableur, sans qu'aucune trace de ce calcul ne subsiste dans la plateforme. C'est un travail manuel lourd, source d'erreurs, à refaire chaque année pour tout le portefeuille.
Aucun moteur de rapprochement. Les provisions vivent dans RentalContract (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:529, effectiveCallForRentalChargesAmount, RentAmount CALL_FOR_RENTAL_CHARGES) ; les réels dans BudgetLine.actualsRecoverableAmounts (adb-parts/src/main/java/fr/lifeconnect/adb/part/model/domain/budget/BudgetLine.java, avec prévisionnel vs réalisé par année et previousYearLine). Grep de régularisation/provision/prorata sur adb-accounting, adb-contracts, adb-parts, adb-aggregates, adb-views, adb-reports : rien ne les joint ; adb-accounting/src/main n'a aucune occurrence de Budget ni d'AllocationKey. Le prorata n'existe que pour la facturation du loyer (RentalContract.proratedMonth), pas pour les quotes-parts de charges pondérées par l'occupation — confirmé par adb-doc/docs/concepts/accounting/engine-audit-2026-07.md:267.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:529adb-parts/src/main/java/fr/lifeconnect/adb/part/model/domain/budget/BudgetLine.javaadb-doc/docs/concepts/accounting/engine-audit-2026-07.md:267
Une fois le solde calculé, il faut facturer le complément au locataire ou lui rembourser le trop-perçu de provisions. La plateforme le permet, mais uniquement par une écriture manuelle : le gestionnaire choisit la nature « régularisation de charges », tape un montant global, un sens et une date — une écriture par locataire et par an, à la main, sans aucun traitement en lot. L'écriture comptable est correcte et arrive bien sur le compte du locataire, mais elle ne porte aucun détail (ni exercice, ni lien avec les factures ou la répartition) et apparaît dans le compte-rendu de gestion (CRG) dans la rubrique « divers » plutôt qu'en charges, ce qui brouille la lecture pour le propriétaire.
Saisie via manualEntry, case RENTAL_CHARGES_ADJUSTMENT (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1165), posting 411100/706200 correct dans PostingRuleFactory.java:469 via ManualEntryEvent, UI dans entry-details.component.ts:85. Un seul montant, pas de lignes de détail, pas d'exercice, pas de lien vers facture/clé de répartition, pas de batch. Dans le CRG l'écriture tombe dans le bucket « divers » (CRGRentalContractEntriesProjection.java:177) et non en charges ; l'event est documenté « En cours » dans adb-doc/docs/concepts/accounting/events/rental-charges-adjustment.md.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1165adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:469adb-ui/src/app/content/components/accounting/accounting-manual-entries/entry-details/entry-details.component.ts:85adb-doc/docs/concepts/accounting/events/rental-charges-adjustment.md
La loi impose de tenir les justificatifs de charges à la disposition du locataire pendant un mois après l'envoi du décompte, avec un décompte par nature de charges. Cette fonctionnalité n'existe pas dans la plateforme : aucun décompte de charges n'est généré, aucun détail par lot n'est imprimable, et rien ne suit l'obligation de mise à disposition d'un mois. Le seul palliatif est de joindre un PDF quelconque à l'écriture manuelle de régularisation. En cas de contestation d'un locataire, le gestionnaire ne peut pas répondre à partir de la plateforme : le décompte et les justificatifs doivent être reconstitués entièrement en dehors de l'outil.
Non développé. Grep de justificatif/décompte/decompte/relevé de charges sur tous les modules : seul FILE_TYPE.PROOF_RESIDENCE existe. adb-reports/src/main/resources/templates/ ne contient que les gabarits RC, RDC, call-for-rent et resumption-of-lease — aucun template de décompte de charges, et aucun type de fichier « charges » dans les TSV du catalogue adb-utilities. Le seul mécanisme est l'upload PDF générique attaché à l'écriture manuelle (adb-ui .../accounting-manual-entries/upload-doc-modal), sans lien avec un quelconque calcul de ventilation.
adb-reports/src/main/resources/templates/
Dernière étape : ajuster les provisions pour charges de l'année suivante à partir des dépenses réelles constatées. La plateforme modélise correctement les provisions comme des lignes à date d'effet sur le bail, donc un nouveau montant peut s'appliquer proprement à partir d'une date donnée — c'est un vrai point positif. Mais le chiffre lui-même est tapé par le gestionnaire : aucune proposition n'est calculée à partir du réel de l'année, aucune mise à jour en masse sur un portefeuille, et la nouvelle provision ne garde aucun lien traçable avec la régularisation qui la justifie. Le recalibrage reste donc un exercice bail par bail, au jugé ou sur la base du tableur de l'étape précédente.
Le modèle est bon : RentalContract.rentAmounts + findEffectiveRentAmount (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:509) gèrent des lignes RentAmount à date d'effet, dont CALL_FOR_RENTAL_CHARGES, sauvegardées via contracts.service.ts:2012 (saveRentAmounts) et éditées dans rc-conditions.component.ts:297. Manque tout le calcul : pas de proposition dérivée des actualsRecoverableAmounts de l'exercice, pas de mise à jour en masse, et aucune référence de la nouvelle ligne vers l'écriture de régularisation.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:509adb-ui/src/app/core/services/contracts/contracts.service.ts:2012adb-ui/src/app/content/components/contracts/shared/rc-conditions/rc-conditions.component.ts:297
Le passage CRG-paiement. La validation d'un CRG ne produit ni montant à payer ni paiement : l'opérateur lit un solde à l'écran, saisit un paiement sortant par propriétaire, exécute le virement hors ADB — ni pain.001, ni lot, ni rapprochement.
L'arrêté d'une période de CRG existe bien dans la plateforme : le gestionnaire propose puis valide la période, et les écritures et factures concernées sont marquées comme rattachées à ce CRG. Mais tout se fait mandat par mandat, à la main : rien ne vérifie que la nouvelle période s'enchaîne sans trou ni chevauchement avec la précédente, et une période déjà validée peut être supprimée sans garde-fou. Concrètement, une erreur de manipulation peut faire disparaître un arrêté de comptes déjà rendu au propriétaire, ou laisser des écritures hors de tout CRG sans que personne ne s'en aperçoive.
Clôture en deux phases réelle par owner+RDC : CRGController.java:52-56 expose /accounting/CRG/owners/{ownerId}/rdcs/{rdcId}/periods/{upFrom}/propose|process et CRGServiceImpl.java:104-131 tague écritures et factures avec le crgDescriptor. Mais CRGPeriodState ne connaît que PROPOSED/PROCESSED, les POST propose/process sautent validateThatCrgIsLastOne (aucun contrôle de continuité ni de chevauchement), et deletePeriod (CRGPeriodCustomRepositoryImpl.java:193-206) fait un simple $unset du descriptor même sur une période PROCESSED.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java:52-56adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:104-131adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CRGPeriodCustomRepositoryImpl.java:193-206adb-ui/src/app/content/components/accounting/crg/crg.component.ts
Le CRG repose bien sur les encaissements réels : la colonne « dû au propriétaire » est construite à partir des loyers effectivement encaissés auprès des locataires, et non des loyers simplement appelés — c'est le bon principe. En revanche, certains flux sont mal traités : les reversements financés par une garantie loyers impayés et certains transferts locataire-vers-propriétaire ne sont pas repris, tandis que les dépôts de garantie conservés par l'agence peuvent apparaître à tort comme dus au propriétaire. Le gestionnaire doit donc contrôler et corriger ces cas à la main sur chaque reddition, sous peine de reverser trop ou trop peu.
CRGRentalContractEntriesProjection.java:236-247 (setSettlementsSummaries) calcule dueToOwner à partir des settlements dont settlingAccountingEventType == TENANT_PAYMENT_RECEIVED — donc bien cash-based. Mais la ligne :241 porte le `// FIXME: add TENANT_TO_OWNER filter when available` : TENANT_TO_OWNER et les reversements financés par GLI sont exclus, et CALL_FOR_DEPOSIT figure dans MISCELLANEOUS_EVENT_TYPES, si bien que les dépôts de garantie conservés par l'agence fuient dans la colonne propriétaire agrégée par CRGPeriodRecapFactoryImpl.java:270-300.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:236-247adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/impl/CRGPeriodRecapFactoryImpl.java:270-300
Le calcul des honoraires de gestion fonctionne correctement : le taux est porté par le mandat, et les factures d'honoraires sont générées automatiquement dès qu'un loyer est appelé ou encaissé, puis comptabilisées sans intervention humaine. Deux points de vigilance à garder en tête : l'assiette déclenchée par l'appel de loyer inclut le dépôt de garantie (l'agence peut donc prélever des honoraires sur une somme qui n'est pas un loyer), et le mécanisme repose sur un composant marqué comme voué à disparaître dans le code — à sécuriser avant toute évolution.
Le taux vit sur le mandat (RentalFeesAmount.java:38-39,96-108, MANAGEMENT_FEES_WITHOUT_VAT en EURO ou PERCENTAGE ; RentalDelegateContract.java:235 getManagementFeesWithoutVat). Les factures d'honoraires sont créées automatiquement par SettlementFeesInvoiceCreationStrategy et CallForRentFeesInvoiceCreationStrategy, déclenchées via AccountingFunctions.java:120-160 (onAccountingEventModified -> batchProcessInvoices) et postées en RENTING_MANAGEMENT_OWNER_FEES_WITHOUT_VAT via InvoiceHelper.java:56-62,150-170. Caveats : l'assiette CFR inclut CALL_FOR_DEPOSIT, et la classe d'événement RentingManagementOwnerFees est @Deprecated(forRemoval).
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/RentalFeesAmount.java:38-39adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/strategy/invoice/creation/impl/fees/SettlementFeesInvoiceCreationStrategy.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/AccountingFunctions.java:120-160adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/InvoiceHelper.java:56-62
Les factures fournisseurs rattachées à un mandat sont bien reprises sur le CRG, ventilées par immeuble et distinguées entre payées et non payées. Mais deux problèmes sérieux : d'abord, un défaut confirmé fait que le paiement d'un fournisseur sur un mandat crédite deux fois la banque et débite deux fois le propriétaire — la comptabilité du bailleur est fausse tant que la correction n'est pas passée à la main. Ensuite, la chaîne travaux (devis, ordres de travaux) vit dans un module absent de la plateforme : rien ne transforme un devis accepté en facture fournisseur, le gestionnaire doit saisir chaque facture à la main et la rattacher au mandat, faute de quoi elle manque silencieusement au CRG.
InvoiceTemplateRepositoryImpl.java (paidCrgInvoicesPerBuilding / notPaidCrgInvoicesPerBuilding) alimente CRGPeriodRecapFactoryImpl.java:155-200 pour la ventilation par immeuble. Bug confirmé F061 (critique) : SUPPLIER_PAYMENT_ISSUED avec rentalDelegateContract (PostingRuleFactory.java:421, cf. SupplierToOwner.java) crédite la banque 2× et double-débite le bailleur. Côté travaux, seul un stub TicketsPartController.java existe dans adb-parts : le service adb-tickets (devis/OT) est absent du monorepo, aucune facture fournisseur n'est générée depuis un devis accepté.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/InvoiceTemplateRepositoryImpl.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/impl/CRGPeriodRecapFactoryImpl.java:155-200adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PostingRuleFactory.java:421adb-parts/src/main/java/fr/lifeconnect/adb/part/controller/TicketsPartController.java
La notion de réserve propriétaire — un montant minimum conservé sur le compte du bailleur avant reversement, pour couvrir les charges à venir — n'existe pas dans la plateforme. Aujourd'hui, la seule façon de retenir de l'argent est soit de passer une écriture de régularisation libre, soit simplement de reverser moins que le solde, sans qu'aucune règle ne le trace ni ne le reconduise d'une période à l'autre. La retenue dépend donc entièrement de la vigilance du gestionnaire, sans contrôle ni historique.
Recherche RESERVE/reserve/retenue/provision/PROVISION dans AccountingEventType.java (67 types), adb-accounting/src/main, adb-contracts/src/main, adb-ui/src et adb-doc/docs/concepts/accounting/events/* : aucun montant de réserve sur RentalDelegateContract, aucun événement comptable de réserve, aucune règle de retenue avant paiement. Le plus proche est AccountingEventType.OWNER_ADJUSTMENT (« Régul. diverses », CATALOG_fr.properties:21), une écriture libre. Si la fonctionnalité était construite, elle vivrait plausiblement comme champ du RentalDelegateContract consommé par la chaîne de paiement OWNER_PAYMENT_ISSUED.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java
Le solde du propriétaire existe et est fiable : un solde courant par mandat est tenu en continu, affiché comme « solde antérieur » sur la reddition et repris dans l'écran de paiement. Ce qui manque, c'est un « net à payer » figé par période : le CRG ne calcule ni ne mémorise le montant à reverser pour la période arrêtée. Le gestionnaire lit donc un solde à l'écran et décide lui-même du montant du virement, sans que la plateforme ne rapproche jamais ce qui a été payé de ce que la période disait devoir.
Un solde auxiliaire 466100 courant par RDC existe : BalanceAggregationOperations.java:131 (qualifier OWNER_GLOBAL sur 466100), exposé par BalanceController.java:43 et consommé par accounting-payment-form.component.ts:241-242 (getContractBalances -> totalAmount) ; CRGPeriodRecapFactoryImpl.java:203-262 l'injecte comme « solde antérieur » (addPreviousBalancesEntries, BALANCE_LABEL). Mais aucun « net à payer » n'est persisté sur le document CRGPeriod : le solde est une balance de compte vivante, pas un résultat de période rattaché au CRG.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/aggregations/impl/BalanceAggregationOperations.java:131adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/impl/CRGPeriodRecapFactoryImpl.java:203-262adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/BalanceController.java:43adb-ui/src/app/content/components/accounting/payment/accounting-payment-form.component.ts:241-242
La reddition des comptes est produite correctement : un vrai relevé de gérance à la française est généré en PDF, avec le bloc légal de l'agence, la garantie financière, le propriétaire, les immeubles et baux, le détail mensuel loyers/charges/encaissé/dû au propriétaire, les factures et le solde antérieur, en version brouillon comme en version définitive. En revanche, le document s'arrête là : rien n'enchaîne vers le paiement, l'archivage ou l'envoi au propriétaire — le gestionnaire télécharge le PDF et fait le reste à la main.
CRGController.java:88-120 (GET .../propose et .../process en application/pdf) délègue à CRGServiceImpl.java:88-101 getPdf, rendu via DocumentServiceImpl.java:139-146 createPdfReport (template tiré du bucket S3 templates) ; CRGPeriodRecapFactoryImpl construit bloc agence, bloc propriétaire et détail par immeuble/bail. Variantes brouillon (PROPOSED+PROCESSED) et définitive (PROCESSED seul). Le PDF est streamé vers un iframe ou téléchargé ; rien ne déclenche paiement, archivage ou envoi (un CRGReportSummary hérité subsiste dans AccountingViewController.java:58-73).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java:88-120adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:88-101adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/document/impl/DocumentServiceImpl.java:139-146adb-views/src/main/java/fr/lifeconnect/adb/views/controller/AccountingViewController.java:58-73
Le reversement au propriétaire est possible mais entièrement manuel : le gestionnaire crée un paiement sortant sur le mandat, tape lui-même le montant, puis le propose et le valide. Aucun fichier de virement bancaire n'est généré, aucun traitement en lot ne permet de payer tous les propriétaires d'un coup, et rien n'empêche de reverser plus que le solde du bailleur — l'agence avance alors la trésorerie. Surtout, le paiement n'est jamais relié au CRG qu'il solde : la plateforme ne sait pas quelle reddition a été payée, et le virement bancaire lui-même se fait hors de l'outil.
PaymentHelper.java:20 mappe OUTGOING sur TYPE_RENTAL_DELEGATE_CONTRACT -> OWNER_PAYMENT_ISSUED, via PaymentController (/propose, /process) et accounting-payment-form.component.ts, montant saisi par l'opérateur. Les seules stratégies de création sont DirectDebit, Manual et Reversed — pas de payment run. Côté SEPA, seuls les XSD pain.008.001.02|10 (prélèvement, côté locataire) existent dans adb-contracts/src/main/resources/xsd ; grep pain.001/CstmrCdtTrfInitn/SepaCreditTransfer/virement sur tout le monorepo : aucune génération de fichier de virement. Aucun garde-fou sur le 466100 (découvert propriétaire possible) et aucun crgDescriptor sur le paiement — rien ne relie le paiement à la période CRG.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/PaymentHelper.java:20adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/PaymentController.javaadb-ui/src/app/content/components/accounting/payment/accounting-payment-form.component.tsadb-contracts/src/main/resources/xsd
Rien n'est prévu pour archiver ou envoyer la reddition des comptes : aucune copie du document remis au propriétaire n'est conservée, et si l'on réédite un CRG plus tard, il est recalculé à partir des données du moment — ce n'est donc pas le document réellement envoyé. L'envoi lui-même n'existe pas non plus, alors que le mandat précise pourtant le mode d'envoi souhaité : le gestionnaire télécharge le PDF et l'expédie par ses propres moyens. Conséquence : pas de preuve de ce qui a été transmis au bailleur, ni d'archive versionnée période par période — une vraie fragilité en cas de litige.
CRGServiceImpl.java:88-101 construit le PDF à la volée sans jamais le persister ; les seuls appels fileClient d'adb-accounting sont des lectures (CRGPeriodRecapFactoryImpl.java:414,418 getFileMetadata) et crg-list.component.ts:downloadCRG régénère à chaque téléchargement. accountingStatementSendingMethod (RentalDelegateContract.java:91) n'est référencé que par le modèle, l'agrégat et les mappers de formulaire UI (grep tous modules) — aucun expéditeur ne le consomme, et aucun template CRG n'existe dans adb/adb-communication. La fonctionnalité vivrait plausiblement dans adb-files (archivage) + adb-communication (envoi).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:88-101adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/impl/CRGPeriodRecapFactoryImpl.java:414adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.java:91adb-ui/src/app/content/components/accounting/crg/crg-list.component.ts
Le mandat enregistre bien une périodicité de reddition et une date de reversement prévue, mais la plateforme n'en fait rien : la suggestion de période suivante est toujours « +1 mois », quelle que soit la fréquence convenue avec le propriétaire, et elle peut même dériver sur les fins de mois. Aucune tâche planifiée n'ouvre ni ne clôture les CRG, et aucune liste de travail n'indique quels propriétaires sont à traiter. Le cycle ne redémarre donc jamais tout seul : le gestionnaire doit se souvenir, propriétaire par propriétaire et mandat par mandat, qu'une reddition est due — avec le risque d'oublis et de redditions en retard vis-à-vis des engagements du mandat.
RentalDelegateContract.java:59 accountStatementFrequency et :69 plannedPayDate ne sont consommés que par RentalDelegateContractAggregate.java:42,48 et les mappers de formulaire adb-ui (contract.mapper.ts:107-113) — aucun moteur ne les lit. CRGServiceImpl.java:241-249 proposeDownTo hardcode plusDays(1)/plusMonths(1) (dérive sur les ancres de fin de mois). Le seul scheduler des deux services est ContractRenewalJobExecutor.java (renouvellement de contrats, adb-contracts) : aucun job n'ouvre/clôture les périodes CRG ni ne produit de worklist des propriétaires dus.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalDelegateContract.java:59adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:241-249adb-contracts/src/main/java/fr/lifeconnect/adb/contract/schedule/impl/ContractRenewalJobExecutor.javaadb-ui/src/app/content/mappers/contract.mapper.ts:107-113
Frontière ticket→comptabilité : aucune référence ticket/devis dans adb-accounting, TicketDetails.repairPayer (TENANT vs OWNER) est un champ mort sans consommateur — la décision récupérable/propriétaire n'atteint jamais le grand livre, chaque montant est ressaisi.
Le signalement d'un incident par un locataire ou un propriétaire devrait pouvoir arriver directement dans la plateforme. Aujourd'hui il n'existe aucun canal d'entrée : pas d'espace locataire, pas de formulaire, pas de traitement automatique des e-mails. Un collaborateur de l'agence doit recevoir l'appel ou le courriel, puis ressaisir lui-même l'incident dans l'outil ; rien de ce que le déclarant a écrit n'est repris automatiquement, avec un risque d'oubli ou de déformation.
Aucun canal d'intake externe : toutes les créations POST /tickets exigent des scopes agence TICKETS_*_CREATE (adb-tickets SecurityConfig.java:67,112) ; adb-ui/src/app/content/components/tenants/ n'offre que des écrans de liste, pas de création côté locataire. Recherches sur 'portal', 'declaration', self-service et tout handler mail/webhook entrant dans adb-tickets : le seul listener est TicketPartFunction.java:41 (@SqsListener sur part-updated). Un intake externe se logerait plausiblement dans adb-tickets ou un futur front public.
adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/config/SecurityConfig.java:67adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/function/TicketPartFunction.java:41adb-ui/src/app/content/components/tenants/
La création du ticket de travaux ou de sinistre fonctionne bien : le ticket est typé (travaux, sinistre), priorisé, numéroté, rattaché au lot, au locataire, au propriétaire et au fournisseur, avec un fil d'événements et des pièces jointes. En revanche, une fois créé, le ticket ne déclenche rien dans le reste de la plateforme : la comptabilité et les autres modules n'en sont jamais informés automatiquement, tout enchaînement reste à la charge du gestionnaire.
Entité de premier rang : Ticket.java et TicketState.java dans adb-tickets, easyId sériel, liens parts/tenant/owner/supplier via TicketDescriptor, délégation à une sous-agence ; typologie catalogue dans CATALOG_fr.properties (TICKET_TYPE.WORK=Travaux, TICKET_TYPE.DAMAGE=Sinistres, TICKET_NATURE.DAMAGE=Sinistre) ; création UI via adb-ui .../tickets/works/create-works-modal/. Limite : aucun publisher SQS/SNS n'existe dans le module — le ticket n'émet aucun événement sortant.
adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/Ticket.javaadb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/TicketState.javaadb-tickets/src/main/resources/CATALOG_fr.propertiesadb-ui/src/app/content/components/tickets/works/create-works-modal/
La demande de devis existe en partie : la plateforme génère bien un courrier PDF adressé au fournisseur (coordonnées, adresse du lot, codes d'accès, contact locataire) et les fournisseurs sont structurés sur le ticket. Mais le devis lui-même n'existe pas comme objet de gestion : pas de montant saisi, pas de comparaison réelle de plusieurs devis — le devis reçu n'est qu'un PDF joint au dossier. Conséquence : le montant accepté ne circule jamais ; personne en aval (accord du propriétaire, facture, comptabilité) ne peut s'appuyer automatiquement dessus.
Le cycle devis n'est que des labels timeline (CATALOG_fr.properties: QUOTE_REQUESTED/RECEIVED/SENT/ACCEPTED/REFUSED/QUOTE_COMPARISON_REQUESTED ; FILE_NATURE.QUOTE=Devis) ; Event.java ne porte que eventType, comment, eventDate, files — pas d'entité Quote, pas de montant. Un vrai PDF de demande d'intervention adressé au fournisseur existe : EventController.java:98 GET /tickets/{id}/events/{eventId}?supplierId → EventServiceImpl.java:222 generateTicketEventPdfRecap (FreeMarker ticket-recap-pdf.ftlh depuis S3, EventRecap fusionnant agence/fournisseur/adresse+codes d'accès/contact locataire), déclenché via tickets/shared/select-supplier-modal ; fournisseurs obligatoires à la création (create-works-modal.component.ts:199). Le montant du devis n'existe donc que dans le PDF uploadé — rien de structuré pour l'aval.
adb-tickets/src/main/resources/CATALOG_fr.propertiesadb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/Event.javaadb-tickets/src/main/java/fr/lifeconnect/adb/ticket/controller/EventController.java:98adb-ui/src/app/content/components/tickets/shared/create-event-modal/create-event-modal.component.ts
L'accord du propriétaire au-delà d'un seuil de travaux (habituellement fixé dans le mandat de gestion) n'existe tout simplement pas dans la plateforme : aucun seuil paramétrable, aucune trace de qui a approuvé quoi, aucun blocage. La seule mention d'un seuil est une phrase figée « 250 € HT » dans le courrier type. Rien n'empêche donc d'émettre un ordre de service sans l'accord du propriétaire, quel que soit le montant — une exposition juridique directe vis-à-vis du mandat, gérée aujourd'hui uniquement par la vigilance des gestionnaires.
NON_DEV confirmé : grep sur tout Java/TS/HTML/properties pour 'threshold', 'plafond', 'worksLimit', 'maxWork', 'approvalLimit', 'ownerApproval', 'OWNER_APPROVAL', 'workAuthorization' — zéro hit ; RentalDelegateContract.java (mandat de gestion) n'a aucun champ travaux/réparations. TICKET_EVENT_TYPE.AGREEMENT_IN_PROGRESS n'est qu'un label. Seule occurrence d'un seuil : la phrase codée en dur « 250 € HT » dans adb-ui/HTML-Template/ticket-recap-pdf.html:621. La fonctionnalité se logerait plausiblement sur RentalDelegateContract + une règle bloquante dans adb-tickets.
adb-ui/HTML-Template/ticket-recap-pdf.html:621
L'ordre de service est un vrai document généré par la plateforme, complet et professionnel (agence, fournisseur, locataire, adresse du lot, codes d'accès, description des travaux). Mais il ne mentionne ni montant ni référence au devis accepté, et surtout il n'est pas envoyé : le gestionnaire le télécharge puis l'expédie lui-même par e-mail au fournisseur, sans aucune trace d'envoi ni d'accusé de réception dans l'outil. En cas de litige avec le fournisseur, la preuve de la commande est dans la messagerie du gestionnaire, pas dans le dossier.
PDF généré via EventController.java:98-105 (GET /tickets/{id}/events/{eventId}, produit application/pdf), modèle EventRecap.java, rendu ITextDocumentGenerator.java (FreeMarker + iText, template tiré de S3) ; déclenchement UI = bouton download sur NOTICE_TO_PROCEED (ticket-events-list.component.html:42, .ts:165-192). Manquent : montant et référence devis dans EventRecap, envoi au fournisseur, accusé, numéro d'OS lié à un devis.
adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/controller/EventController.java:98adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/EventRecap.javaadb-tickets/src/main/java/fr/lifeconnect/adb/ticket/service/document/impl/ITextDocumentGenerator.javaadb-ui/src/app/content/components/tickets/shared/ticket-events-list/ticket-events-list.component.ts:165
Le suivi de l'intervention repose sur le gestionnaire, qui ajoute à la main des étapes datées et commentées au dossier (intervention planifiée, travaux en cours, travaux effectués, intervenant relancé). La plateforme sait déléguer un ticket à un prestataire externe avec des droits limités dans le temps, et le ticket porte un budget travaux et des dates — mais il n'y a ni planning, ni délai contractuel surveillé, ni relance automatique d'un intervenant en retard : la relance reste un geste manuel. Et une fois les travaux terminés, rien ne prépare la facture fournisseur attendue : elle devra être saisie de zéro.
Progression = événements timeline libres (CATALOG_fr.properties: PLANNED_INTERVENTION, ONGOING, INTERVENTION_DONE, DUNNED_INTERVENTOR) ; Event.java n'a ni date planifiée ni assignee. Une délégation scoped et bornée existe pourtant : TicketDelegationController.java (POST/GET/DELETE /tickets/{ticketId}/delegations), DelegationCreationServiceImpl.java accorde ALL_TICKETS_WORKS_SCOPES pour les tickets WORK, DelegationEntity porte startDate/endDate/delegatedScopes (documenté adb-doc/docs/architecture/auth.md:138, couvert par ticket_delegation_access.feature) ; rôle technicien avec scopes lecture ticket + création événement (ScopesByRoles.java:485) ; TicketDetails porte workBudget/workAmountWithoutTaxes/WithTaxes. Manquent : scheduling, SLA, relance automatique, et aucun écran de délégation dans adb-ui.
adb-tickets/src/main/resources/CATALOG_fr.propertiesadb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/Event.javaadb-doc/docs/architecture/auth.md:138
L'enregistrement de la facture fournisseur est solide en tant qu'acte comptable : vraie écriture en partie double, TVA par ligne, distinction facture/avoir, ventilation par lot ou par clé de répartition, facture scannée jointe. Le problème est qu'elle vit en vase clos : la comptabilité ignore tout du ticket. Impossible de rapprocher la facture de l'ordre de service ou du devis accepté — le contrôle « facture conforme au devis » est entièrement manuel, avec un risque réel de payer un montant supérieur à ce qui avait été validé.
Comptabilisation via InvoicePostingRuleFactory.java:27-30 (incomingSupplierInvoice → 6xxxxx / 401000), modèle Invoice.java (supplier, flowDirection, billingNature invoice/avoir, items), InvoiceStateMachine.java ; UI adb-ui .../accounting/invoices-content/ (upload PDF FILE_TYPE.INCOMING_INVOICE, recherche fournisseur, ventilation UNIT/ALLOCATION_KEY/LINE, compte 6xxxxx par ligne) ; documenté dans adb-doc/docs/concepts/accounting/events/incoming-supplier-invoice.md. Rupture : InvoiceOrigin.java n'a pas de valeur TICKET et aucun ticketId n'existe dans adb-accounting — aucun rapprochement ticket/OS/devis possible.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/factory/InvoicePostingRuleFactory.java:27adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Invoice.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/statemachine/InvoiceStateMachine.javaadb-doc/docs/concepts/accounting/events/incoming-supplier-invoice.md
Décider si une réparation est récupérable sur le locataire ou à la charge du propriétaire devrait piloter la suite (refacturation, régularisation de charges). Aujourd'hui le champ prévu à cet effet sur le ticket est inerte : le gestionnaire ne peut même pas le renseigner à l'écran, et rien ne le lit. En pratique, la récupérabilité se joue implicitement dans le choix du compte de charge au moment de la saisie de la facture, et les budgets de charges récupérables ne sont jamais alimentés par les factures réellement comptabilisées. Conséquence : aucune détection automatique des sommes récupérables — des montants dus par les locataires peuvent passer à la trappe.
TicketDetails.java:80-84 porte repairPayer (@CatalogValue TICKET_REPAIR_PAYER) et repairPayerDetails, mais grep repo-wide repairPayer/REPAIR_PAYER ne retourne que le modèle adb-tickets, TicketDetailsEntity.java:69 et les tests unitaires — pas de champ dans ticket-information.component.html, aucun consommateur dans adb-accounting/adb-contracts/adb-views. Côté parts, Charge.java:39 isRecoverable et BudgetLine.actualsRecoverableAmounts existent mais BudgetServiceImpl.java:271-383 alimente les réalisés par saisie opérateur, et adb-parts n'a aucun @SqsListener sur les événements comptables.
adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/TicketDetails.java:80adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/charges/Charge.java:39adb-parts/src/main/java/fr/lifeconnect/adb/part/service/impl/BudgetServiceImpl.java:271
La refacturation au locataire de la part récupérable existe, mais uniquement sous forme d'écriture manuelle : le gestionnaire choisit le bail, la nature, le sens, et retape le montant. Pas de prorata, pas de règle de TVA, pas de clé de récupération partielle, aucun lien avec la facture fournisseur ni avec le ticket. C'est la troisième ressaisie du même montant (devis, puis facture, puis régularisation locataire), et rien ne signale une facture récupérable qui n'aurait jamais été refacturée — perte de recettes silencieuse possible.
Seuls points d'entrée : RentalContract.java:1159 manualEntry() switch, cas REPAIRS_DUE_BY_TENANT (:1200) et REPAIRS_DUE_BY_OWNER (:1193) ; proposés dans l'écran d'écriture manuelle entry-details.component.ts:76,87. Documenté dans adb-doc/docs/concepts/accounting/events/repairs-due-by-tenant.md. Aucune génération depuis une facture ou un ticket, aucun contrôle de complétude.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1159adb-ui/src/app/content/components/accounting/accounting-manual-entries/entry-details/entry-details.component.ts:76adb-doc/docs/concepts/accounting/events/repairs-due-by-tenant.md
Le paiement du fournisseur fonctionne correctement : il est comptabilisé, lettré avec la facture, et déclenche automatiquement la mise à charge du propriétaire — c'est ce qui fait apparaître la dépense sur son compte-rendu de gestion sans intervention manuelle. Seule limite pratique : la plateforme sait générer des fichiers de prélèvement pour les locataires, mais pas de fichier de virement fournisseur ; le virement lui-même se fait donc côté banque.
Payment.java:245 (SUPPLIER_PAYMENT_ISSUED, 401000 / 512101), PaymentHelper.java:21, chaîne post-lettrage PostSettlementsProcessorImpl.java:50 qui génère automatiquement SupplierToOwner.java — documenté dans adb-doc .../supplier-payment-issued.md. La seule logique SEPA trouvée (PaymentRepository.forSepa, PAYMENT_METHOD.DIRECTDEBIT) concerne le prélèvement locataire ; aucune génération de fichier de virement fournisseur.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/Payment.java:245adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/PostSettlementsProcessorImpl.java:50adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/events/SupplierToOwner.javaadb-doc/docs/concepts/accounting/events/supplier-payment-issued.md
Le compte-rendu de gestion reflète bien les factures fournisseurs payées et les régularisations de réparations saisies à la main : sur ce périmètre, il est fiable. En revanche, tout ce qui vit côté ticket n'y arrive jamais : budget travaux, honoraires de suivi de travaux, indemnité d'assurance, montant du sinistre — et surtout toute somme récupérable que le gestionnaire aurait oublié de ressaisir. Le CRG est donc juste par rapport à la comptabilité, mais aveugle par rapport au dossier travaux.
Projections CRG : CRGInvoiceProjection.java (paidAmount, amountDue, unitMortgageNumber) et CRGRentalContractEntriesProjection.java:168,178 (REPAIRS_DUE_BY_TENANT / REPAIRS_DUE_BY_OWNER dans MISCELLANEOUS_EVENT_TYPES), servies par CRGController.java. Aucune donnée d'adb-tickets (workBudget, honoraires travaux, insuranceIndemnity, damageTotalAmount) n'alimente ces projections.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGInvoiceProjection.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/projections/CRGRentalContractEntriesProjection.java:168adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java
Le suivi d'un sinistre se limite à des champs saisis à la main sur le ticket (numéro de sinistre, type et origine du dommage, assureur, indemnité, montant total) et à des étapes de suivi dans le fil du dossier. La déclaration n'est pas transmise à l'assureur par la plateforme, aucune échéance d'expertise n'est surveillée, et l'indemnité attendue n'existe pas en comptabilité comme créance. Quand l'indemnité arrive, elle doit être comptabilisée par une écriture manuelle : le CRG du propriétaire ne compense donc jamais automatiquement le coût du sinistre par l'indemnité reçue, et une indemnité jamais réclamée ne sera signalée par rien.
TicketDetails.java porte claimNumber, damageType, damageOrigin, insuranceInCharge, insuranceIndemnity, damageTotalAmount, impactOnValuation ; étapes en labels catalogue (INSURANCE_DECLARATION, INSURANCE_EXPERT_VISIT_PENDING/PLANNED, INVOICE_TO_INSURANCE, INSURANCE_WORKS_DONE) ; insuranceIndemnity n'est qu'un input numérique libre (ticket-information.component.html:332). Côté comptable, AccountingEventType.java:135-140 : les seuls événements assurance sont GLI_* (garantie loyers impayés) — aucun événement typé pour un sinistre dommages ; l'indemnité reçue passe par écriture manuelle.
adb-tickets/src/main/java/fr/lifeconnect/adb/ticket/model/TicketDetails.javaadb-ui/src/app/content/components/tickets/shared/ticket-information/ticket-information.component.html:332adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java:135
La chaîne EDL de sortie → retenues → solde de tout compte : EDL réduit à un drapeau conforme/non-conforme sans comparaison à l'entrée, retenues manuelles libres, aucun objet ni document d'arrêté des comptes — restitution calculée entièrement hors plateforme.
La réception d'un congé doit être enregistrée qu'il vienne du locataire ou du bailleur, avec sa preuve d'envoi (LRAR). La plateforme ne sait enregistrer que le congé donné par le locataire : le congé délivré par le bailleur (préavis de 6 mois, motif reprise ou vente) n'existe pas dans l'outil, et aucun suivi de la lettre recommandée n'est prévu — la lettre est au mieux un fichier déposé à la main. Le gestionnaire doit donc gérer les congés bailleur et la preuve de notification entièrement hors plateforme, avec un risque juridique si la date ou la preuve de réception est contestée.
ContractNotifyEventInfo (adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/event/ContractNotifyEventInfo.java) ne modélise que le départ locataire, avec les catalogues TENANT_LEAVE_NON_COMPLIANCE_REASON / TENANT_LEAVE_SHORTEN_REASON ; le modal UI s'appelle rc-departure-notice-modal. La transition SIGNED→NOTICED est câblée dans RentalContractStateMachineConfig.java:88 et traitée dans ContractStateServiceImpl.java:292,343. Aucune entité congé bailleur, aucun document de congé, aucun suivi AR/LRAR.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/event/ContractNotifyEventInfo.javaadb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:292adb-ui/src/app/content/components/contracts/display-rental-contract/active-state/modals/rc-departure-notice-modaladb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/RentalContractStateMachineConfig.java:88
Le délai de préavis (1 ou 3 mois selon le type de bail, la zone tendue ou le motif de préavis réduit) devrait être déduit automatiquement et contrôlé. Aujourd'hui, c'est le gestionnaire qui choisit lui-même le délai parmi trois options (1 mois, 3 mois, préavis réduit), et la plateforme n'effectue en réalité aucun contrôle : le contrôle de conformité du préavis réduit existe dans le code mais n'est jamais activé. Un délai erroné (par exemple 1 mois accordé à tort) passe donc sans alerte, avec un risque de loyers perdus ou de litige. Le préavis fait néanmoins l'objet d'un calcul de date de fin transmis automatiquement à la suite du dossier.
ContractHelper.java:105-155 (adb-contracts) ne supporte que MONTH(1), QUARTER(3) et SHORTENED, choisis par radio dans rc-departure-notice-modal.component.html. checkShortenedNoticePeriod est du code mort : les deux points d'entrée dans ContractStateServiceImpl.java:355-372 passent validation=false, et isCompliant/nonComplianceReason sont déclarés sans jamais être vérifiés. Aucune dérivation depuis la catégorie de bail, la zone tendue ou le justificatif de préavis réduit ; toute endDate saisie par l'opérateur écrase silencieusement le calcul.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/helper/ContractHelper.java:105-155adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:355-372adb-ui/src/app/content/components/contracts/display-rental-contract/active-state/modals/rc-departure-notice-modal/rc-departure-notice-modal.component.html
La date de sortie du locataire est bien gérée : elle est enregistrée sur le bail, peut être modifiée ou annulée si le locataire retire son congé, et c'est cette date unique qui pilote automatiquement le calcul des derniers loyers. Ce maillon fonctionne correctement et s'enchaîne sans intervention manuelle.
contract.end est un champ de première classe posé par setTheoreticalEndDate/setEnd dans ContractStateServiceImpl.java:355-372 (avec override endDateIsDifferent), modifiable via le modal rc-modify-exit-date-modal et annulable par l'événement REVOKE_NOTICE (ContractRevokeNoticeEventInfo). C'est la seule date consommée en aval par le moteur de loyers.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:355-372adb-ui/src/app/content/components/contracts/display-rental-contract/active-state/modals/rc-modify-exit-date-modal
Le dernier loyer doit être calculé au prorata des jours réellement occupés. Le moteur de quittancement sait faire ce prorata et génère automatiquement l'appel de loyer et de charges correspondant. Mais un défaut confirmé rend la dernière période à jamais infacturable pour les baux payés à terme échu au trimestre ou à l'année : pour ces baux, le dernier loyer n'est tout simplement jamais appelé. C'est de l'argent dû mais jamais réclamé, sauf si un gestionnaire détecte le trou et facture à la main.
RentalContract.java construit les périodes bornées par getEnd() (:589,606,624), calcule proratedRent (:869) via isLastCallForRentPeriod (:1456) et proratedMonth() (:1715) ; les événements CALL_FOR_RENT / CALL_FOR_RENTAL_CHARGES partent automatiquement via ContractsFunctions.onContractModified → contractCallForRentCallBack (adb-accounting). Défaut confirmé F045 : la dernière période n'est jamais facturable pour les baux trimestriels/annuels à terme échu.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1456adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1715adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/function/ContractsFunctions.java
L'état des lieux de sortie devrait être un document structuré : pièce par pièce, état de chaque élément, photos, vétusté, signatures. Dans la plateforme, l'EDL de sortie se résume à une date et une case « conforme / non conforme » ; le document lui-même n'est qu'un PDF déposé en pièce jointe, sans détail exploitable. Ce verdict conforme/non conforme sert uniquement à fixer le délai de restitution du dépôt de garantie ; tout le détail des dégradations doit être ressaisi à la main si l'on veut s'en servir, et l'étape suivante ne reçoit donc pas ces informations automatiquement.
ContractEndEventInfo (adb/adb-model) ne porte que inventoryDateOut et le flag INVENTORY_OUT_COMPLIANCE, catalogue à exactement 2 valeurs (CATALOG_fr.properties:1712-1713) ; RentalContract.java:450,471 stocke ces champs. Aucune entité EDL structurée (pièces, éléments, photos, vétusté), aucun workflow de signature ni parcours huissier ; le PDF est un fichier générique FILE_NATURE.CHECK_OUT (CATALOG_fr.properties:1556-1557). Modals UI : rc-add-inventory-date, rc-confirm-inventory-modal.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/state/event/ContractEndEventInfo.javaadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:450adb-ui/src/app/content/components/contracts/display-rental-contract/active-state/modals/rc-confirm-inventory-modal
La comparaison entre l'état des lieux d'entrée et celui de sortie est le fondement juridique de toute retenue sur le dépôt de garantie. Cette fonction n'existe pas du tout dans la plateforme : aucun écran, aucun calcul, aucune trace. Le comparatif est réalisé entièrement en dehors de l'outil (papier, prestataire EDL ou tableur), et rien ne vient donc justifier ni chiffrer automatiquement une retenue dans les étapes suivantes.
Recherche exhaustive (etatDesLieux, EtatDesLieux, INVENTORY_OF_FIXTURES, inventoryItem, « état des lieux », EDL, degradation, vetuste, dilapidation) sur tout le Java/TS/properties : seuls existent RentalContract.inventoryDateIn (une simple date, RentalContract.java:424), FILE_NATURE.CHECK_IN et les événements comptables d'honoraires EDL (adb-doc/docs/concepts/accounting/events/renting-inventory-*-fees-without-vat.md). Aucun modèle d'item, aucun diff, aucun écran ni API de comparaison ; la fonctionnalité vivrait plausiblement dans adb-contracts + adb-ui.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:424
Une retenue sur dépôt de garantie doit être justifiée (constat d'EDL, devis, facture) et plafonnée au montant du dépôt. Dans la plateforme, une retenue n'est qu'une écriture comptable saisie librement par le gestionnaire : montant libre, sens débit/crédit libre, sans lien avec un constat, un devis ou une facture, sans pièce justificative obligatoire et sans plafond. Chaque retenue reste une ligne isolée que rien n'agrège dans un calcul de restitution : le gestionnaire doit faire l'addition lui-même, avec un risque d'erreur de montant et de contestation faute de justificatif rattaché.
Les retenues passent par RentalContract.manualEntry() (RentalContract.java:1159-1210) vers les événements REPAIRS_DUE_BY_TENANT, TENANT_ADJUSTMENT et RENTAL_CHARGES_ADJUSTMENT, tous manual:true à montant et sens libres dans adb-doc/docs/concepts/accounting/events/_events.json. Grep retenue/withhold/holdback/RETENTION : aucun concept métier de retenue, aucun objet de justification, aucune exigence de pièce jointe, aucun plafonnement au dépôt.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1159-1210adb-doc/docs/concepts/accounting/events/_events.json
Au départ du locataire, il faut régulariser les charges sur la fraction d'année occupée (provisions versées contre charges réelles), avec la possibilité légale de retenir 20 % du dépôt jusqu'à l'arrêté des comptes de copropriété. La plateforme dispose bien d'une écriture comptable typée « régularisation ponctuelle de charges » qui fonctionne, mais elle ne calcule rien : pas de comparaison provisions/réel, pas de prorata sur la période occupée, et le mécanisme de retenue de 20 % en attente de la régularisation copro n'existe nulle part. Le gestionnaire doit faire tout le calcul dans un tableur puis reporter le résultat à la main, et le solde de sortie ne reçoit donc pas cette régularisation automatiquement.
Grep regulariz/régularis, CHARGES_REGULARIZATION, « 20% », RETENTION sur tout le Java : zéro moteur (les seuls hits « 20% » sont des largeurs de colonnes UI, rc-conditions.component.ts:87). Il existe toutefois un posting typé opérationnel : ONE_TIME_CALL_FOR_RENTAL_CHARGES (AccountingEventType.java:133, préfixe OTCFRC, statut « Développé » dans _events.json, comptes 411100/706200, libellé « Charges locatives - régularisation ponctuelle »), et adb-parts porte BudgetLineEntity.recoverable + ChargeEntry.recoverableShare. Manquent uniquement le calcul provisions vs réel, le prorata et la retenue de 20 % ; RENTAL_CHARGES_ADJUSTMENT reste manual:true, statut « En cours » (_events.json:9).
adb-doc/docs/concepts/accounting/events/_events.jsonadb-ui/src/app/content/components/contracts/display-rental-contract/active-state/rc-conditions.component.ts:87
L'arrêté des comptes du locataire sortant (solde de tout compte) devrait produire un montant net à restituer et un document remis au locataire. La plateforme calcule bien un solde comptable du locataire et déduit automatiquement la date limite légale de restitution (1 ou 2 mois selon la conformité de l'EDL), mais il n'existe aucun document d'arrêté, aucun état « comptes arrêtés », et cette date limite n'est affichée nulle part ni utilisée par personne. Concrètement, le montant à restituer est déterminé hors système, et le locataire ne reçoit aucun décompte produit par la plateforme : la restitution ne s'enchaîne pas automatiquement.
Grep « solde de tout compte », « arrêté des comptes », settlement statement, tenantStatement sur Java/TS/i18n : rien. adb-reports/src/main/resources/templates/ ne contient que RC.html, RDC.html, call-for-rent.html, resumption-of-lease.html et les .ftlh de bail ; le CRG (CRGRentalContractEntriesProjection.java) est côté propriétaire uniquement. En revanche, RentalContract.endContractSettlementTerm est auto-calculé (ContractAfterMapper.java:314, inventoryDateOut + CONFIG_PARAM.RENTAL_CONTRACT_INVENTORY_SETTLEMENT_COMPLIANT/NON_COMPLIANT, 1 vs 2 mois — délai loi 89 art. 22) et la position du locataire est un solde calculé (BalanceController, LedgerAuxiliaryAccountService.rentalContractInfo, adb-accounting) ; mais aucun document ni état ne les consomme.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/ContractAfterMapper.java:314adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/BalanceController.javaadb-reports/src/main/resources/templatesadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/CRGRentalContractEntriesProjection.java
Le dépôt de garantie doit être restitué sous 1 mois (EDL conforme) ou 2 mois (non conforme). La plateforme calcule bien cette échéance légale, mais ne l'affiche nulle part, ne crée aucune tâche et n'envoie aucun rappel : personne n'est alerté quand la date approche ou est dépassée, ce qui expose directement à la pénalité légale de retard. Le remboursement lui-même est une écriture saisie à la main, sans montant proposé (dépôt moins retenues), sans garde-fou sur le montant ou le sens, et uniquement pour les dépôts détenus par l'agence : une erreur de saisie part au grand livre sans contrôle.
L'échéance est dérivée à la lecture dans ContractAfterMapper.java:273,314-335 (endContractSettlementTerm = inventoryDateOut + RENTAL_CONTRACT_INVENTORY_SETTLEMENT_COMPLIANT/_NON_COMPLIANT ; cf. ContractAccountingService.java:40-52, RentalContract.java:478) mais jamais rendue dans adb-ui (seul hit : rental-contract-model.ts:116), jamais persistée en tâche, et aucun job ni notification ne la consomme. Le versement est TENANT_DEPOSIT_REIMBURSEMENT, manual:true (adb-doc/.../events/tenant-deposit-reimbursement.md, RentalContract.java:1179) — F002 : remboursement DG non gardé (ni détenteur, ni solde, ni plafond), échéance calculée jamais alertée.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/mapper/ContractAfterMapper.java:273adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/ContractAccountingService.java:40-52adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:1179adb-doc/docs/concepts/accounting/events/tenant-deposit-reimbursement.md
La loi prévoit qu'un dépôt de garantie restitué en retard entraîne une majoration de 10 % du loyer mensuel par mois de retard entamé, au bénéfice du locataire. Cette pénalité n'existe nulle part dans la plateforme : rien ne détecte le retard, rien ne la calcule, et elle ne peut même pas être comptabilisée autrement que par une écriture libre non typée. Si un locataire la réclame, tout le chiffrage et la comptabilisation se font à la main, sans traçabilité dédiée.
Grep penalt/PENALT/majoration/indemnité sur Java et properties : seuls hits DOCUMENT_LAYOUT_METADATA.PENALTY_SURCHARGE (« pénalité de retard (%) », champ de clause du PDF de bail, CATALOG_fr.properties:1421) et un texte de clause 10 % dans infra/seeds/data/adb-reports/template_meta_data.json. Aucun AccountingEventType dédié, aucun calcul, aucun déclencheur ; seule voie de comptabilisation : TENANT_ADJUSTMENT non typé. La fonctionnalité vivrait plausiblement côté adb-accounting (nouvel événement typé) déclenchée depuis adb-contracts.
infra/seeds/data/adb-reports/template_meta_data.json
La clôture du bail existe et fonctionne proprement, mais elle reste entièrement manuelle : c'est le gestionnaire qui clôt le bail depuis une fenêtre de l'application, sans qu'aucune vérification n'impose au préalable un solde locataire à zéro ni un dépôt de garantie restitué, et aucune tâche planifiée ne clôt les baux dont la date de fin est passée. Un bail terminé sans congé préalable doit passer par un autre circuit (résiliation). La clôture ne déclenche par ailleurs aucun traitement du dépôt, des charges ou du décompte : ces suites restent à faire à la main.
Transition NOTICED --END--> ENDED dans RentalContractStateMachineConfig.java:108, traitée par ContractStateServiceImpl.java:386-392 (setContractInventoryDateOut), déclenchée à la main via rc-dates-bloc.component.ts:200-262 endContractManually et contracts.service.ts:879-915. Aucune garde sur solde 411 nul ni DG soldé ; le seul scheduler d'adb-contracts est ContractRenewalJobExecutor.java (renouvellement, pas clôture). Un bail sans congé préalable doit passer par TERMINATE.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/RentalContractStateMachineConfig.java:108adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:386-392adb-ui/src/app/content/components/contracts/display-rental-contract/rc-dates-bloc.component.ts:200-262adb-ui/src/app/core/services/contracts/contracts.service.ts:879-915
Étapes 5-6 (DG et soldes locataires sortants). Les 7 événements ACCOUNTING_TAKEOVER_* modélisent la reprise entrante ; aucun événement miroir, ni compte de destination, ni type de bénéficiaire pour l'agence sortante : chaque euro sort en écriture non typée.
Mettre fin à un mandat de gestion devrait déclencher tout le processus de sortie : motif de résiliation, repreneur, validation, et clôture des baux rattachés. Aujourd'hui, la plateforme sait seulement enregistrer une date de fin en coulisses ; l'écran du mandat n'offre aucun bouton de résiliation, le bloc « date de fin » est en lecture seule. Aucun contrôle n'est fait avant la résiliation et rien n'est propagé aux baux, à la comptabilité ni aux autres modules : le gestionnaire doit orchestrer toute la sortie à la main, avec un risque d'oublis (baux restés actifs, écritures qui continuent).
La transition SIGNED -(TERMINATE)-> TERMINATED existe dans RentalDelegateContractStateMachineConfig.java:88-91 (seule sortie métier), mais ContractStateServiceImpl.java:291,330-334 ne fait qu'updateTerminatedDate : pas de motif, pas de repreneur, pas de cascade vers les RC (ContractStateServiceImpl.java:455-482 n'enrichit que les baux SIGNED). ContractValidatorFactory.java ne fournit des validateurs rentalDelegateContract que pour DRAFT et READY_TO_SIGN — aucun validateur TERMINATED. Côté adb-ui, un grep TERMINATE sous rentals-delegates/ ne retourne que du CSS de badge de statut : aucune action ni modale de résiliation ; rdc-end-date.component.html est un affichage en lecture seule. La résiliation n'émet rien : ni événement comptable, ni message vers parts/persons/files.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/RentalDelegateContractStateMachineConfig.java:88-91adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:291adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/ContractValidatorFactory.java
La dénonciation d'un mandat (préavis du propriétaire ou de l'agence, opposition à la tacite reconduction) devrait être enregistrée avec sa date, son délai de préavis et un suivi du courrier recommandé. Cette fonction n'existe tout simplement pas : la mécanique de préavis présente dans la plateforme ne concerne que les congés des locataires. L'équipe gère donc la dénonciation du mandat entièrement hors outil (courrier, agenda, tableur), et rien dans la plateforme ne calcule la date de fin effective ni ne prépare le dernier CRG ou l'arrêté des comptes.
ContractEvent.NOTIFY/REVOKE_NOTICE existent mais ContractStateServiceImpl.java:343-344,375 les garde par checkArgument(isRentalContract(contract)) — baux locataires uniquement ; RentalDelegateContractStateMachineConfig.java n'a aucune transition NOTIFY et n'atteint jamais NOTICED. Un grep monorepo sur résiliation/préavis/dénonciation/revocation ne remonte que de la prose de clauses dans adb-reports/src/main/resources/templates/rentalDelegateContract.ftlh:1375,1403,1456. Côté natures de fichiers (adb-utilities/CONTRACTS-values-input.tsv), RENTAL_LEAVE et RENT_END_PERIOD_SENT/RECEIVED existent mais aucune nature de courrier de résiliation de mandat. La fonctionnalité vivrait plausiblement dans la state machine RDC + un objet notice porté par le contrat.
adb-contracts/src/main/java/fr/lifeconnect/adb/contract/service/impl/ContractStateServiceImpl.java:343-344adb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/RentalDelegateContractStateMachineConfig.javaadb-reports/src/main/resources/templates/rentalDelegateContract.ftlh:1375adb-utilities/CONTRACTS-values-input.tsv
À la fin du mandat, un CRG de clôture devrait arrêter les comptes du propriétaire : dernière période calée sur la date de fin, inclusion forcée de tous les éléments résiduels, puis verrouillage. La production des CRG ordinaires fonctionne bien, mais la notion de « CRG final » n'existe pas : le gestionnaire lance simplement un CRG normal de plus et le déclare final, sans garantie que la période colle à la date de fin du mandat ni que rien ne traîne. Rien n'empêche non plus de générer par erreur une période supplémentaire après la résiliation, et le solde restant du propriétaire n'est repris automatiquement dans aucune action de règlement ou de clôture.
CRGController.java:49-56 gère les périodes par owner+rdc (propose/process) et CRGPeriodState.java ne connaît que PROPOSED et PROCESSED — aucun état/flag « final ». CRGServiceImpl.java:242-245 calcule la période suivante comme upFrom précédent +1 mois, sans borne sur la terminatedDate du RDC : rien ne force l'alignement sur la fin du mandat ni ne bloque une période post-résiliation. La sortie est un PDF/récapitulatif ; le solde résiduel affiché n'est enchaîné vers aucun règlement ni clôture (handoff manuel).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java:49-56adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/CRGPeriodState.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.java:242-245
Avant de clore un mandat, il faut solder les comptes : reverser au propriétaire ce qu'on lui doit (ou récupérer ce qu'il doit) et faire le point sur les impayés locataires de son portefeuille. Le gestionnaire peut effectuer ces paiements avec les outils habituels, mais la plateforme ne calcule aucun « solde de tout compte », n'empêche pas de résilier un mandat alors que des soldes subsistent, et ne signale pas les impayés locataires du propriétaire sortant. Le contrôle repose entièrement sur la vigilance humaine : un solde oublié, c'est de l'argent dû non reversé ou non récupéré, sans état transmis au repreneur.
PaymentHelper.java:20 route une cible RDC vers OWNER_PAYMENT_ISSUED / OWNER_PAYMENT_RECEIVED : le règlement passe par un paiement ordinaire. Rien ne calcule un solde de tout compte, aucun validateur TERMINATED n'existe sous validation/rentalDelegateContract/ pour bloquer la résiliation tant que 466100/416100 sont non nuls, et OWNER_PAYMENT_ISSUED n'a aucun garde contre le tirage à découvert du 466100 (engine-audit-2026-07.md:137). Aucun rapport de solde résiduel n'est produit et rien ne relie le règlement au mandat.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/PaymentHelper.java:20adb-doc/docs/concepts/accounting/engine-audit-2026-07.md:137adb-contracts/src/main/java/fr/lifeconnect/adb/contract/validation/rentalDelegateContract/
Quand un mandat part chez un autre administrateur, les dépôts de garantie des locataires doivent lui être transférés, avec une trace par locataire. La plateforme ne prévoit rien pour cela : les dépôts ne peuvent sortir du compte de l'agence que par un remboursement au locataire. Pour les remettre au repreneur, le comptable doit bricoler une écriture manuelle ou un faux paiement fournisseur, sans traçabilité par locataire — impossible ensuite de réconcilier le registre des dépôts, alors que ces fonds sont réglementés (garantie financière, pointage par locataire exigible en cas de contrôle).
Dans AccountingEventType.java, les événements dépôt sont CALL_FOR_DEPOSIT (entrée, ligne 14), TENANT_DEPOSIT_REIMBURSEMENT (retour locataire, ligne 84) et ACCOUNTING_TAKEOVER_TENANT_DEPOSIT (reprise depuis l'ancien gestionnaire, ligne 128) — aucun événement sortant symétrique. PaymentHelper.java:20 n'offre que des bénéficiaires owner/tenant/supplier, pas d'agence tierce. Un grep TRANSFER_OUT/HANDOVER/deposit transfer sur tous les modules Java ne retourne rien : la seule voie est une écriture MANUAL/MANUAL_ENTRY non typée.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/PaymentHelper.java:20
En miroir de la reprise de comptes à l'entrée d'un mandat (qui, elle, est complète dans la plateforme : soldes débiteurs/créditeurs, loyers, charges, écrans dédiés), la sortie devrait permettre de transférer proprement les comptes locataires au repreneur. Cette sortie n'existe pas : aucun événement, aucun compte, aucun écran. L'intention avait pourtant été identifiée (deux ébauches jamais branchées subsistent dans le paramétrage). En pratique, les comptes locataires du portefeuille sortant ne peuvent pas être soldés de façon propre et le repreneur n'a aucun état de reprise fourni par l'outil pour se réconcilier.
La reprise IN est complètement modélisée : les 7 événements ACCOUNTING_TAKEOVER_* (AccountingEventType.java:125-131) sont tous entrants, avec règles d'imputation dans RentPostingRuleFactory.java:304-398, comptes 4672xx dédiés (LedgerAccountShort.java:145-184) et règles UI (takeover-ui-rules.md). L'image miroir sortante n'existe pas. Deux placeholders dormants : TRANSFER_BALANCE_SHEET (AccountingEventType.java:107) déclaré et référencé nulle part ailleurs (grep repo-wide), et CONTRACT_ORIGIN.TRANSFER (CONTRACTS-values-input.tsv:260, « Contrat transféré ») sans aucune référence code, alors que CONTRACT_ORIGIN.TAKE_OVER pilote de la vraie logique (RenewalEmptyTakeOverContractAction, IndexQualifier.TAKE_OVER).
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java:107adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/rule/RentPostingRuleFactory.java:304-398adb-utilities/CONTRACTS-values-input.tsv:260adb-doc/docs/concepts/accounting/takeover-ui-rules.md
À la fin du mandat, l'agence doit remettre au propriétaire ou au repreneur le dossier complet : bail, états des lieux, diagnostics, quittances, attestations. Les documents sont bien stockés et consultables un par un dans la plateforme, mais il n'existe ni constitution d'un « dossier de transfert », ni téléchargement groupé par propriétaire, ni liste de contrôle de complétude, ni trace de ce qui a été remis et quand. Le gestionnaire télécharge donc les pièces une à une et tient le bordereau de remise à la main — sans preuve opposable de la restitution en cas de litige.
Dans adb-files/adb-files-app/src/main/java, un grep zip/bulk/archive/export ne trouve aucun endpoint d'export groupé ou d'archive (seuls MetaDataUpdateRepository / FileServiceImpl matchent incidemment). Aucune nature de fichier « remise/attestation de transfert » dans adb-utilities/*-values-input.tsv (grep attestation/handover/FILE_NATURE.*), et aucun modèle de document de remise dans adb-reports/src/main/resources/templates/. Rien n'enregistre la remise, donc rien ne peut conditionner la clôture du compte propriétaire à celle-ci (handoff manuel).
adb-files/adb-files-app/src/main/javaadb-reports/src/main/resources/templates/
Une fois le mandat soldé, le compte du propriétaire devrait être clôturé pour qu'il disparaisse des listes actives et qu'aucune écriture ne puisse plus y atterrir. Cette clôture n'existe pas : le compte propriétaire reste ouvert indéfiniment et continue d'apparaître dans les balances, les listes de CRG et les écrans de paiement, et la fiche du propriétaire ne peut pas non plus être archivée. Risque concret : une écriture ou un paiement peut être imputé par erreur à un propriétaire parti depuis des mois, et les listes de travail se remplissent de comptes morts.
AbstractLedgerAccount.java:48-52 porte un flag Boolean active, mais LedgerAccountController / LedgerAuxiliaryAccountController n'exposent aucun endpoint de clôture/désactivation (seulement GET + POST de création pour contrats/personnes) : le compte auxiliaire 466100/RDC reste ouvert. Côté adb-persons, aucun archive/deactivate sur les personnes (UserController.deleteUser vise un utilisateur Keycloak, pas un propriétaire). Le flag 'open' de FiscalYear.java est à l'échelle de l'exercice, pas du propriétaire. Rien ne signale aux services aval la fin de la relation.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/AbstractLedgerAccount.java:48-52adb-persons/src/main/java/fr/lifeconnect/adb/person/controller/adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/FiscalYear.java
À la sortie du mandat, les lots concernés devraient être désactivés ou marqués « restitués » pour sortir des listes de travail. Or un lot n'a aucun cycle de vie propre dans la plateforme : son statut affiché n'est que le reflet de l'état de ses contrats, et le statut « restitué » n'est atteignable que par une manipulation d'administration, sans parcours ni écran pour le déclencher. Conséquence : les lots des mandats terminés continuent d'apparaître dans les portefeuilles, l'état locatif et les écrans de relance, sauf à forcer manuellement chaque contrat dans un état terminal — un bruit permanent qui masque le vrai portefeuille actif.
AbstractPart.java n'a aucun champ status/active/archived (seulement inMaintenance, ligne 73) et PartController.java n'expose aucun mapping DELETE/désactivation (les deletes n'existent que sur BudgetController et ChargeController). Le statut de lot affiché est dérivé côté UI de l'état du contrat rattaché (parts.mapper.ts:558 : HANDED_OVER/SOLD/TERMINATED). Le ContractState HANDED_OVER n'est atteignable que via la transition de reset admin SET_HANDED_OVER (ContractFlowUtils.java:144-169) — aucun événement métier HAND_OVER, aucun wizard, aucune UI ne le produit.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/AbstractPart.javaadb-parts/src/main/java/fr/lifeconnect/adb/part/controller/PartController.javaadb-ui/src/app/core/mappings/parts.mapper.ts:558adb-contracts/src/main/java/fr/lifeconnect/adb/contract/statemachine/ContractFlowUtils.java:144-169
Clôture d'exercice/période. `FiscalYear.open` n'est jamais lu et aucun code ne ferme une période (FiscalYearServiceImpl : list/lookup/create seulement) — grand livre ouvert en permanence : ni solde de clôture, ni à-nouveaux, ni archivage, ni inaltérabilité.
La saisie quotidienne des écritures fonctionne bien : chaque événement de gestion (loyer, honoraires, paiement, régularisation) génère automatiquement une écriture en partie double, avec journal, compte du plan comptable, numéro de pièce et ventilation de la TVA. Les opérations diverses saisies à la main passent par le même circuit de contrôle que les écritures automatiques, ce qui garantit leur cohérence. Le renouvellement des contrats alimente lui aussi ce moteur via une tâche planifiée. C'est un socle solide et fiable du cycle comptable.
Vrai moteur événementiel en partie double : chaîne de handlers typés dans adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/ (DefaultHandlerChain, FiscalYearHandler, PeriodHandler, JournalHandler, LedgerAccountHandler, PostingRuleHandler, PieceReferenceHandler, TransactionSequenceHandler), TVA ventilée via TaxCalculator, séquence par organisation, plan PCG seedé (445660/445710/512101 dans infra/seeds/data/adb-accounting/ledger_account.json). Les OD manuelles sont des événements MANUAL/MANUAL_ENTRY (AccountingEventType.java:47,53) et empruntent la même chaîne de règles ; ContractRenewalJobExecutor alimente le moteur côté contrats.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.javainfra/seeds/data/adb-accounting/ledger_account.jsonadb-ui/src/app/content/pages/accounting/accounting-manual-entries-page/
Le lettrage existe en partie : les règlements de factures et les prélèvements sont rapprochés automatiquement de leurs écritures, et un rapprochement manuel est possible. Mais la plateforme n'attribue aucun code de lettrage (exigé par le fichier FEC en cas de contrôle fiscal), ne permet pas d'annuler un lettrage erroné, et surtout une dette rouverte par une contre-passation reste invisible : le gestionnaire peut croire un compte soldé alors qu'il ne l'est pas. Ce résultat n'est en outre jamais transmis automatiquement au rapprochement bancaire ni à aucun export.
Les settlements sont embarqués sur l'écriture (AccountingEntry.java:137, getOpenAmount/getSettledAmount) et créés automatiquement via les stratégies strategy/settlement/creation/impl/ (Manual, InvoicePayment, DirectDebitPayment), avec un flux propose/process (SettlementController.java:41,52 et SettlementServiceImpl). Manquent : le code de lettrage (champs FEC 14-15, marqués « Pending » dans adb-doc/docs/concepts/accounting/fec.md), un endpoint de dé-lettrage, et un état de lettrage partiel exploitable ; F053 : les dettes rouvertes par contre-passation ne sont pas visibles côté lettrage.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/SettlementServiceImpl.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/SettlementController.java:41adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/AccountingEntry.java:137adb-doc/docs/concepts/accounting/fec.md
Le rapprochement bancaire n'existe pas : aucun relevé bancaire ne peut être importé dans la plateforme. Le compte banque n'est mouvementé que par les opérations internes, et rien ne permet de marquer une écriture comme « rapprochée ». Concrètement, le comptable ressaisit chaque ligne du relevé à la main dans les écrans d'encaissement et de décaissement, et les prélèvements rejetés (impayés SEPA) ne reviennent jamais automatiquement dans la comptabilité : ils passent inaperçus tant que personne ne compare le relevé au grand livre.
Recherche repo-wide (hors node_modules) de bankStatement|releve.?bancaire|statementImport|camt|mt940|ofx|csvImport|pain.002|impaye sur *.java/*.ts/*.html : zéro résultat. PaymentOrigin.java ne connaît que MANUAL, INSURANCE, INVOICE, DIRECT_DEBIT, REVERSE ; le 512101 n'est mouvementé que par événements internes. Seul le flux sortant SEPA existe (DirectDebitXmlExport, pain.008, côté adb-contracts) — aucun traitement des retours pain.002/camt.054, aucun flag de rapprochement, aucun compte d'attente.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/PaymentOrigin.java
La clôture de période est à moitié construite : si une période est marquée fermée, la plateforme refuse bien toute nouvelle écriture dedans — le verrou fonctionne. Mais aucun écran ni aucune fonction ne permet de fermer une période : l'indicateur est posé une fois pour toutes à la création de l'exercice et n'est jamais modifié ensuite. Pour clôturer un mois, il faut qu'un développeur intervienne directement dans la base de données, sans trace de qui a fermé quoi et quand — un vrai risque de conformité comptable.
L'enforcement existe : PeriodHandler.java:34-49 rejette toute écriture hors période ouverte (FiscalYearPeriod.open). Mais rien ne mute ce flag : FiscalYearController.java:35,43 n'expose que GET et POST-create (pas de PUT/PATCH), grep de setOpen sur les périodes dans adb-accounting/src/main : néant, et le seed infra/seeds/data/adb-accounting/fiscal_year.json livre tous les mois avec open:true. adb-ui ne lit /accounting/years que comme filtre (JournalsService.ts:884).
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/PeriodHandler.java:34adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/FiscalYearController.java:35adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/model/FiscalYearPeriod.javainfra/seeds/data/adb-accounting/fiscal_year.json
La clôture d'exercice n'est que partiellement couverte. Le verrou fonctionne : une écriture dans un exercice ou un mois fermé est bien bloquée. Mais aucune opération de clôture n'existe : pas de contrôles préalables (équilibre des comptes, périodes toutes fermées), pas de détermination du résultat de l'exercice (comptes 120/129), et fermer l'exercice suppose une intervention manuelle en base par un développeur. Aucun solde de clôture n'est produit : il n'y a rien à transmettre à l'exercice suivant.
FiscalYearServiceImpl expose exactement trois méthodes (fiscalYears, fiscalYear, create) ; fiscalYearRepository.save n'est appelé que depuis create ; grep de yearEndClos|closeFiscal|yearClos repo-wide : zéro. Le verrou est toutefois réel (correction post-vérification) : FiscalYearRepository.fiscalYear() filtre {'open': true} et PeriodHandler.java:37 filtre FiscalYearPeriod::isOpen (« No open period found ») — mais basculer open=false reste une édition Mongo manuelle (cf. adb-infrastructure/mongodb-atlas/data-scripts/refresh_fiscal_year.js). La détermination du résultat (120/129) et les à-nouveaux sont totalement absents.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/FiscalYearServiceImpl.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/handler/PeriodHandler.java:37adb-infrastructure/mongodb-atlas/data-scripts/refresh_fiscal_year.js
La reprise des à-nouveaux n'existe pas : rien ne génère le journal d'ouverture de l'exercice N+1 à partir des soldes de clôture de l'exercice N. Les comptes de bilan continuent simplement de s'accumuler d'année en année dans la même base, et les états sont calculés par plage de dates plutôt qu'à partir de soldes reportés. Tout cabinet comptable externe qui reçoit les données doit reconstituer lui-même les à-nouveaux, un travail manuel et source d'écarts à chaque début d'exercice.
Grep repo-wide de carryForward|carry_forward|openingBalance|aNouveau|a_nouveau|retainedEarnings : les seuls hits sont des soldes d'ouverture de rapport CRG dans adb-views/.../repository/AccountingEventRepository.java:184-386 (agrégations locataire/propriétaire pour le PDF CRG), pas des écritures comptables. Aucun AccountingEventType d'écriture d'ouverture dans adb/adb-model/.../AccountingEventType.java ; le grand livre est requêté par plage de dates.
adb-views/src/main/java/fr/lifeconnect/adb/views/repository/AccountingEventRepository.java:184adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java
L'export FEC, obligatoire en cas de contrôle fiscal, n'existe pas. La plateforme sait exporter le grand livre en tableur, mais ce fichier ne couvre qu'environ 12 des 19 colonnes réglementaires : il manque notamment le lettrage, la date de validation et les champs devise, et le format (tableur à libellés français) n'est pas le fichier texte normé au nom réglementaire exigé par l'administration. En cas de vérification, le gestionnaire ne peut pas produire de fichier conforme depuis la plateforme : le tableur doit être retravaillé à la main ou par l'expert-comptable.
Grep repo-wide de EcritureNum|JournalCode|CompteNum|PieceRef|EcritureLib|CompAuxNum|ValidDate|Montantdevise dans les sources main : aucun code de production (seulement des noms de champ pieceReference) ; l'intention n'est documentée que dans adb-doc/docs/concepts/accounting/fec.md. L'artefact le plus proche est l'export XLSX du grand livre : AccountingEntryServiceImpl.java:41-61 (HEADER_KEYS) + :121 entriesAsXlsx, modèle LedgerAccountingEntry, exposé par AccountingEntriesController.java:53, consommé par info-summary-gl.component.ts:131 (avec confirmation UI au-delà de 500 lignes, :507). Manquent EcritureLet, DateLet, ValidDate, Montantdevise/Idevise, l'utilisateur, et le format texte SIRENFECYYYYMMDD.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/AccountingEntryServiceImpl.java:41adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/AccountingEntriesController.java:53adb-ui/src/app/content/pages/accounting/general-ledger/info-summary-gl/info-summary-gl.component.ts:131adb-doc/docs/concepts/accounting/fec.md
La déclaration de TVA (CA3) n'est pas prise en charge. La TVA est bien calculée et comptabilisée écriture par écriture, mais rien ne totalise les comptes de TVA collectée et déductible dans les cases de la CA3 : pas de période déclarative, pas de choix entre TVA sur encaissements et sur débits, pas de télétransmission. Le comptable lit les soldes de TVA sur l'export du grand livre, ressaisit la CA3 dans un outil externe, puis doit repasser à la main l'écriture de liquidation de TVA dans la plateforme.
Grep repo-wide de \bCA3\b|declaration.?tva|vatReturn|teledeclaration : zéro résultat. La TVA n'existe qu'au niveau des écritures : adb-accounting/.../helper/TaxCalculator.java et comptes 445660 (déductible) / 445710 (collectée) dans infra/seeds/data/adb-accounting/ledger_account.json:395,419. Aucune agrégation 4457x/4456x vers des cases CA3, aucune période déclarative, aucun événement d'écriture de liquidation.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/helper/TaxCalculator.javainfra/seeds/data/adb-accounting/ledger_account.json:395
La déclaration DAS2 (honoraires versés à des tiers) est entièrement hors plateforme. Rien ne suit le seuil de déclaration, ne vérifie la complétude des SIRET des bénéficiaires, ni ne cumule les honoraires versés par tiers et par année ; aucun fichier déclaratif n'est généré. L'équipe doit extraire les paiements fournisseurs à la main et les recouper avec les fiches des tiers pour bâtir la DAS2 dans un outil externe — un travail annuel fastidieux et exposé aux oublis.
Grep repo-wide de DAS2|das2 sur *.java/*.ts/*.html/*.json : zéro résultat. La matière première existe (SUPPLIER_PAYMENT_ISSUED et événements d'honoraires dans adb/adb-model/.../AccountingEventType.java, compte fournisseur 401100, tiers dans adb-persons), mais rien ne marque un tiers comme déclarable DAS2 ni ne cumule les montants versés par SIRET et par année.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/accounting/AccountingEventType.java
Le relevé fiscal du propriétaire (revenus fonciers 2044/2072) n'existe pas. La plateforme produit bien le CRG, un compte rendu de gestion en PDF par propriétaire et par période de mandat (loyers encaissés, charges, honoraires, reversement), mais ce n'est pas un état fiscal : aucune ventilation en recettes brutes, frais et charges déductibles, intérêts d'emprunt ou travaux, aucune correspondance avec les lignes de la 2044 ou de la 2072, aucun récapitulatif annuel. La qualification fiscale de chaque ligne reste entièrement à la charge du propriétaire ou de son comptable.
Grep repo-wide de 2044|2072|revenus fonciers|revenu foncier : zéro résultat. Ce qui existe est le CRG : CRGController.java (propose/process/PDF), CRGServiceImpl, CRGPeriod + CRGPeriodState (PROPOSED/PROCESSED), agrégation dans adb-views/.../repository/AccountingEventRepository.java, UI adb-ui/src/app/content/pages/accounting/crg/. C'est un état de gestion, pas un état fiscal : aucune catégorisation fiscale des lignes.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.javaadb-accounting/src/main/java/fr/lifeconnect/adb/accounting/service/impl/CRGServiceImpl.javaadb-views/src/main/java/fr/lifeconnect/adb/views/repository/AccountingEventRepository.javaadb-ui/src/app/content/pages/accounting/crg/
L'inaltérabilité des écritures est à moitié assurée. Le bon côté : une écriture ne peut être ni modifiée ni supprimée par les écrans, toute correction passe par une contre-passation, et une numérotation continue existe — l'historique des actions est également conservé. Le côté manquant : pas de scellement des écritures (aucune empreinte ou signature garantissant l'absence d'altération), pas de date de validation, pas d'archivage figé d'un exercice clos, et le seul mécanisme de verrouillage (les périodes) n'est jamais activé en pratique. Certains documents générés (CRG, factures) restent par ailleurs modifiables ou supprimables — une exigence de conformité qui n'est donc que partiellement tenue.
Immutabilité côté API : AccountingEntriesController est GET-only, corrections via adb-accounting/.../model/reversal/ et AccountingEventController + la page UI reverse-accounting-event ; séquence via CustomSequenceRepoImpl.java:40 (organisationId + année civile) ; historique via adb/adb-action-logs. Contre-preuves : aucun chaînage/hash ni signature, pas de champ ValidDate ; CRGController.java:175 expose un DELETE de période, InvoiceTemplateRepositoryImpl.java:139 un remove ; PeriodHandler ne bloque jamais en pratique (aucune période fermée) ; infra/src/stacks/logs.js:20 n'applique l'object-lock S3 qu'aux buckets CloudTrail, pas aux PDF CRG/factures.
adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/CustomSequenceRepoImpl.java:40adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/controller/CRGController.java:175adb-accounting/src/main/java/fr/lifeconnect/adb/accounting/repository/impl/InvoiceTemplateRepositoryImpl.java:139infra/src/stacks/logs.js:20
L'étape d'alerte d'expiration : adb-files NotifyAboutExpiredCertificatesJob a son @Scheduled commenté et ne souscrit jamais son Flux ; l'aval complet (adb-notifications -> email de relance adb-reports) n'a aucun producteur. Chaque échéance dépend de la mémoire humaine.
Chaque année, l'avis de taxe foncière / TEOM doit être enregistré et ventilé par immeuble et par lot pour préparer la refacturation. Aujourd'hui, l'avis n'est qu'un PDF rangé dans le dossier d'une personne, sans montant, sans millésime et sans distinction récupérable / non récupérable. La refacturation mensuelle de la TEOM au locataire fonctionne bien une fois le montant saisi sur chaque bail, mais c'est au gestionnaire de lire l'avis, de calculer la quote-part de chaque lot et de ressaisir le montant bail par bail — aucune transmission automatique entre le document archivé et les baux.
L'avis n'existe que comme document (FILE_TYPE.TAX + FILE_NATURE.PROPERTY_TAX/HOUSING_TAX, cibles Person* uniquement, infra/seeds/data/adb-utilities/configurations.json:283,424,431). Le modèle Charge d'adb-parts (Charge.java, PARTS_CHARGES_NATURE.TAXES, isRecoverable/recoverableShare) est un îlot de données : aucun lecteur dans adb-accounting ni adb-contracts. La refacturation TEOM est en revanche un vrai flux typé : champ de bail CONTRACTS.RENT.CURRENT_HOUSEHOLD_WASTE_TAX (rc-conditions.component.html:577) → RentAmount CFR_CHARGES_HOUSEHOLD_WASTE (RentalContract.java:535, RentPostingRuleFactory:593-611), appelé mensuellement (manual=false, recurrent=true), crédit 706200, repris au CRG (CRGRentalContractEntriesProjection:231). Seuls l'avis et sa ventilation par lot restent manuels.
infra/seeds/data/adb-utilities/configurations.json:283adb/adb-model/src/main/java/fr/lifeconnect/adb/model/part/charges/Charge.javaadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:535
La quote-part récupérable de la TEOM devrait pouvoir être refacturée à tous les locataires d'un portefeuille en une seule opération annuelle. La provision mensuelle par bail existe et s'appelle automatiquement avec chaque quittancement, mais la régularisation annuelle face à l'avis réel est une écriture comptable manuelle, contrat par contrat. Il n'existe ni écran ni traitement de masse : sur un portefeuille de plusieurs centaines de lots, cela représente une campagne de saisie entièrement manuelle chaque automne.
La ligne récurrente CFR_CHARGES_HOUSEHOLD_WASTE (AccountingEventType, RentalContract.java:533-537,626,1221,1297) est appelée avec chaque avis d'échéance. La régularisation passe par les événements manuels HOUSEHOLD_WASTE_TAX_DUE_BY_TENANT et ONE_TIME_CFR_CHARGES_HOUSEHOLD_WASTE (manual=true dans adb-doc/docs/concepts/accounting/events/_events.json), saisis bail par bail. Grep de bulk|massive|campaign dans adb-accounting et adb-contracts : uniquement du bulkWrite Mongo, aucun traitement métier de masse.
adb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/RentalContract.java:533adb-doc/docs/concepts/accounting/events/_events.json
La plateforme devrait suivre la validité de tous les diagnostics obligatoires (DPE, ERP, amiante, plomb, électricité, gaz) et signaler ceux qui arrivent à échéance. Le catalogue des types de diagnostics est complet et le gestionnaire peut saisir une période de validité au dépôt du document, mais ces dates sont facultatives et jamais exploitées : seul le DPE sert réellement (pour la règle d'encadrement des loyers des passoires énergétiques). Un diagnostic amiante ou ERP périmé passe totalement inaperçu, avec un risque juridique à la relocation ; le gestionnaire doit tenir son propre échéancier hors outil.
Le catalogue FILE_NATURE couvre tout (DPE_ENERGY/DPE_GREENHOUSEGAS/DPE_GLOBAL, ASBESTOS, LEAD, ELECTRICITY_INSTALLATION, GAS_INSTALLATION, NATURAL_RISK, CARREZ, BOUTIN, NOISE, TERMITE, SERPULA, DTG — infra/seeds/data/adb-utilities/configurations.json). FileMetaData.java:72 porte un lifePeriod saisi par l'opérateur mais optionnel et non validé (diagonistics-shared.component.ts:109-115). Seul le DPE est exploité via energyClassKWH/gasClassKWH (EnergyDiagnosticsExistAssessor, DiagnosticsInfoServiceImpl.java). La requête d'expiration ne cible que targetType=Contract pour assurance/ramonage/chaudière — jamais les diagnostics de Part.
infra/seeds/data/adb-utilities/configurations.jsonadb-files/adb-files-app/src/main/java/fr/lifeconnect/adb/file/FileMetaData.java:72adb-contracts/src/main/java/fr/lifeconnect/adb/contract/DiagnosticsInfoServiceImpl.java
Une relance automatique devrait prévenir le gestionnaire avant l'expiration d'un document réglementaire (assurance, entretien chaudière, ramonage, diagnostics). Cette alerte n'existe pas en production : la chaîne d'envoi d'e-mails a été construite de bout en bout mais la tâche planifiée qui devait la déclencher a été désactivée et n'a jamais été remise en service — et même réactivée, elle ne couvrirait que trois types de documents. Concrètement, aucun rappel ne part jamais ; le seul signal restant est une date affichée en rouge sur la fiche d'un bail, à condition que quelqu'un l'ouvre. Le suivi des échéances repose donc entièrement sur la vigilance humaine.
NotifyAboutExpiredCertificatesJob.java:53 : l'annotation @Scheduled est commentée (ticket LCS-1426) alors que la config est vivante (application.yml:66-70, enabled: true, cron 0 0 9 * * *). Même décommenté, le job serait inerte : le Flux.concat de :58-77 n'est jamais souscrit et aucun publisher de message n'est injecté, donc aucun ExpiredCertificateMessage ne part. Le côté consommateur est pourtant complet (FilesFunctions.java:24 dans adb-notifications, CertificateExpiredEmailNotificationGeneratorImpl dans adb-reports). Aucun trigger Atlas temporel n'existe (les 18 triggers de atlas-triggers.md sont tous change-stream). Périmètre limité à MULTI_RISKS_RESIDENCE, CHIMNEY_MAINTENANCE, BOILER_MAINTENANCE sur les contrats.
adb-files/adb-files-app/src/main/java/fr/lifeconnect/adb/file/job/NotifyAboutExpiredCertificatesJob.java:53adb-notifications/src/main/java/fr/lifeconnect/adb/notifications/FilesFunctions.java:24adb-reports/src/main/java/fr/lifeconnect/adb/reports/CertificateExpiredEmailNotificationGeneratorImpl.javaadb-doc/docs/concepts/atlas-triggers.md
Chaque année, le locataire doit fournir son attestation d'assurance habitation (MRH), et le gestionnaire doit détecter les baux non couverts pour relancer. La plateforme permet de déposer l'attestation et d'en saisir la date de fin à la main, et affiche un signal rouge quand elle est périmée — mais uniquement sur la fiche du bail concerné, quand on l'ouvre. Il n'existe aucune liste des baux dont l'assurance est échue à l'échelle du portefeuille, ni relance automatique : repérer un défaut d'assurance impose d'ouvrir chaque bail un par un, avec le risque de laisser des logements non assurés sans le savoir.
Upload et dates de validité saisies manuellement dans rc-certificates-modal.component.ts:88-100, avec boutons Add/Renew (rc-dates-tenant-certificates.component.html). Le drapeau isExpired est calculé côté navigateur, à l'ouverture du bail, avec un préavis d'un mois (display-rental-contract.component.ts:104-122). Grep de isExpired dans adb-ui : uniquement la page contrat et les réglages agence — aucune vue portefeuille, aucun producteur de relance côté serveur.
adb-ui/src/app/rc-certificates-modal/rc-certificates-modal.component.ts:88adb-ui/src/app/display-rental-contract.component.ts:104
Quand un locataire ne renouvelle pas son assurance habitation, la loi permet au bailleur de souscrire une assurance pour son compte et de lui refacturer la prime majorée jusqu'à 10 %. Ce mécanisme n'existe pas du tout dans la plateforme : ni ligne de refacturation dédiée, ni calcul de la majoration, ni écriture comptable prévue. Si le cabinet devait l'appliquer, tout serait géré hors outil ou par une écriture libre, sans traçabilité du dispositif légal.
Rien dans le catalogue des 67 événements comptables (adb-doc/docs/concepts/accounting/events/_events.json) ni dans le code : grep de insuranceOnBehalf, 'pour compte', 10%, LANDLORD_INSURANCE sur Java/TS sans résultat. InsuranceContract (adb/adb-model) est GLI-only en pratique — gli-dto.ts:28 code en dur INSURANCE_CONTRACT_NATURE.GLI. Une implémentation devrait ajouter un événement comptable dédié et une ligne récurrente de bail, à l'image de CFR_CHARGES_HOUSEHOLD_WASTE.
adb-doc/docs/concepts/accounting/events/_events.jsonadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/InsuranceContract.javaadb-ui/src/app/GLI/gli-dto.ts:28
La CAF demande chaque année au bailleur une attestation de loyer pour maintenir le versement des aides au logement. La plateforme sait très bien encaisser et rapprocher les allocations reçues de la CAF, mais elle est incapable de produire l'attestation elle-même : aucun modèle de document n'existe. Le formulaire est donc rempli à la main, hors outil, à partir de données de loyer que la plateforme possède pourtant déjà — un travail répétitif et un risque d'incohérence avec les montants réellement quittancés.
Grep de attestation, CAF, 'allocation logement', APL sur les templates et le code : adb-reports/src/main/resources/templates/ et infra/seeds/data/adb-reports/template_meta_data.json ne contiennent aucun document de ce type. Tout le code CAF existant est côté entrant : RentalAllowanceContract (adb-contracts), RentalAllowanceStatement* (adb-accounting), caf-history (adb-ui), reçus PAYMENT_METHOD.CAF. La feature vivrait naturellement dans adb-reports comme nouveau template alimenté par l'agrégat contrat.
adb-reports/src/main/resources/templates/infra/seeds/data/adb-reports/template_meta_data.json
Le quittancement mensuel est le point fort de ce processus : chaque appel de loyer génère automatiquement un PDF et un e-mail par bail, et les quittances des périodes passées sont récupérables à la demande. Le mode d'envoi de la quittance est même exigé avant la signature du bail. Une réserve : un seul et même modèle de document sert à la fois de quittance de loyer et d'avis d'échéance, si bien que la distinction payé / impayé n'est pas garantie par le système — une quittance ne devrait juridiquement être délivrée que pour un loyer effectivement encaissé, et ce contrôle repose sur le gestionnaire.
GET /{id}/RentReceipt/{period} sur ContractAggregateController.java:60 ; template unique call-for-rent.html:297 titré « Quittance de loyer et avis d'échéance » (aussi callForRent.ftlh:356) ; e-mail via ContractsFunctions.java:24 (onContractCallForRent → sendCallForRentEmail), piloté par les événements comptables sur le bus de messages. rentReceiptSendingMethod est obligatoire avant signature (RentalContract.java:401, RentalContractReadyToSignValidator.java:102-106). Le moteur ne conditionne pas le libellé quittance au statut d'encaissement.
adb-aggregates/src/main/java/fr/lifeconnect/adb/aggregates/ContractAggregateController.java:60adb-views/src/main/resources/templates/call-for-rent.html:297adb-notifications/src/main/java/fr/lifeconnect/adb/notifications/ContractsFunctions.java:24adb-contracts/src/main/java/fr/lifeconnect/adb/contract/RentalContractReadyToSignValidator.java:102
L'assurance propriétaire non occupant (PNO) devrait être suivie comme une police à part entière : dates de couverture, échéance de renouvellement, refacturation de la prime au propriétaire. Aujourd'hui, la plateforme permet seulement d'archiver la police en pièce jointe et d'enregistrer la prime comme une facture fournisseur saisie à la main chaque année. Aucune date d'échéance n'est suivie : une PNO expirée est invisible pour l'outil, et c'est au gestionnaire de tenir le calendrier de renouvellement et de ressaisir la facture annuelle pour qu'elle apparaisse au CRG du propriétaire.
La PNO n'existe que comme nature documentaire (FILE_NATURE.NON_OCCUPANT_OWNER, infra/seeds/data/adb-utilities/configurations.json:494,550,1215,1250 ; MULTI_RISKS_CONTRACT :172,795). La prime passe par INCOMING_SUPPLIER_INVOICE (manual=true, dont le businessCase cite explicitement la PNO dans _events.json). InsuranceContract (adb/adb-model) offre un catalogue de natures mais l'UI n'instancie que la GLI — aucune entité de police PNO avec dates de couverture, aucun contrôle d'expiration.
infra/seeds/data/adb-utilities/configurations.json:494adb-doc/docs/concepts/accounting/events/_events.jsonadb/adb-model/src/main/java/fr/lifeconnect/adb/model/contract/InsuranceContract.java
Cahier des charges : « Fait » — Audit : Aucun moteur de calcul ; saisie manuelle par locataire, calcul au tableur
Cahier des charges : « Fait » — Audit : apportionAmount stocké mais jamais consommé par aucune logique
Cahier des charges : « Fait » — Audit : Fenêtre de tagging qui fuit + colonne « dû au bailleur » fausse (F062/F063)
Cahier des charges : « Fait » — Audit : Garde-fou présent mais seuils DPE obsolètes (GES 56 ≠ 70, F017)
Cahier des charges : « Fait » — Audit : TVA 20 % en dur jamais en 4457xx ; honoraires sur encaissements débranchés ; primes GLI buggées
Cahier des charges : « Fait » — Audit : Erreurs avalées confirmées : indexation, feedback CFR, bordereau CAF payé avec lignes en erreur
Le logiciel n'a aucun champ pour le loyer de référence majoré ni le complément de loyer, et ne vérifie jamais qu'un nouveau bail ou un renouvellement respecte le plafond applicable à Paris, Lille, Lyon, Bordeaux ou Montpellier. Aujourd'hui, les gestionnaires doivent contrôler ces plafonds à la main, en dehors de l'outil, avant chaque signature ; le bail imprimé ne mentionne qu'une phrase générique. Un dépassement expose la société à des amendes et à des demandes de remboursement de la part des locataires. Avec cette fonction, l'outil bloquerait ou signalerait tout loyer au-dessus du plafond et ferait figurer les mentions obligatoires sur le bail.
Absence d'encadrement des loyers (loyer de référence majoré / complément de loyer)
RentalContract (adb/adb-model .../contract/RentalContract.java) ne comporte aucun champ pour le loyer de référence, le loyer de référence majoré, le complément de loyer ou la zone d'encadrement ; aucun validateur dans adb-contracts/.../validation/rentalContract ne compare le loyer initial ou de renouvellement à un plafond de référence. Le modèle de bail (adb-reports/.../templates/RC.html ligne 1468) n'imprime qu'un texte statique 'Loyer mensuel hors complément de loyer éventuel'. Pour les portefeuilles institutionnels à Paris, Lille, Lyon, Bordeaux, Montpellier, etc., chaque nouveau bail/renouvellement doit afficher et respecter le loyer de référence majoré ; les infractions exposent à des amendes jusqu'à 15 k€ par bail. Rien ne l'impose ni même ne l'enregistre.
L'outil bloque bien l'indexation du loyer (IRL) des passoires énergétiques, mais rien n'empêche de créer et signer un bail sur un logement classé G, pourtant interdit à la location depuis janvier 2025 (et F dès 2028). L'outil ne connaît d'ailleurs pas les classes A à G du DPE : il ne stocke que des consommations brutes, sans la double lecture énergie/gaz à effet de serre du DPE 2021 ni la date de validité du diagnostic. Aujourd'hui, c'est donc au gestionnaire de vérifier lui-même, dossier par dossier, qu'il ne reloue pas un logement interdit — avec un risque juridique direct (bail contestable, loyer non exigible) si un cas passe entre les mailles.
Absence d'interdiction de louer pour un DPE G ; le modèle DPE est antérieur à la réforme de 2021
Le gel des loyers pour les passoires énergétiques existe : EnergyClassRule (adb/adb-model .../rule/impl/energy/) bloque l'indexation lorsque energyClassKWH > 331 ou gasClassKWH > 56 (RentCalculationDiagnosticsInfo, règle débutant le 2022-08-24). Mais rien ne bloque la création/signature d'un bail sur un logement classé G (critère de décence depuis 2025-01, seuil 450 kWh/m²/an, classe F à partir de 2028). Le modèle de données ne stocke que des valeurs brutes en kWh (DiagnosticsInfo.java) — aucune lettre de classe DPE A–G, aucune classe fondée sur les GES (double seuil du DPE 2021), aucune gestion de la date de validité du DPE. RentalContractReadyToSignValidator n'effectue aucun contrôle énergétique.
L'outil ne sait pas quelles communes sont en zone tendue ou soumises à l'encadrement des loyers : le préavis réduit du locataire doit être choisi à la main, sans garde-fou, et rien ne signale les obligations locales. Les démarches de permis de louer et de déclaration de mise en location (autorisation préalable, récépissé, délais) ne sont gérées nulle part : les équipes les suivent aujourd'hui sur tableur ou de mémoire. Une mise en location sans autorisation dans une commune concernée expose à des amendes importantes. Un référentiel des communes permettrait d'appliquer automatiquement les bonnes règles et de suivre ces démarches avec leurs échéances.
Absence de référentiel des zones tendues, pas de permis de louer / déclaration de mise en location
Aucun module ne détient de référentiel commune/zone (zone tendue, zone très tendue, zones d'encadrement). Les conséquences se propagent : impossible d'appliquer automatiquement le préavis réduit (RENTAL_CONTRACT_NOTICE_PERIOD.SHORTENED existe dans le catalogue mais reste un choix purement manuel), impossible de signaler les obligations d'encadrement, impossible de gérer les workflows de permis de louer ou de déclaration de mise en location (autorisation préalable, récépissé, délais) exigés dans de nombreuses communes où opèrent les portefeuilles institutionnels — aucun code, clé de catalogue, nature de fichier ou documentation ne les mentionne. adb-utilities ne récupère que les indices INSEE, pas les données de zonage géographique.
L'outil sait stocker les diagnostics (DPE, amiante, état des risques, électricité, gaz, mesurages…) avec leurs dates de validité, mais il ne vérifie jamais, au moment de la signature, que les annexes obligatoires sont présentes et encore valables. L'alerte automatique sur les documents expirés a par ailleurs été désactivée, et elle ne couvrait de toute façon que les assurances et entretiens de chaudière ou cheminée, pas les diagnostics. Aujourd'hui, ce contrôle repose entièrement sur la vigilance des gestionnaires ; un bail signé avec un DPE périmé expose l'agence à des sanctions et à la contestation du bail par le locataire.
Validité des diagnostics/annexes non contrôlée ; alertes d'expiration désactivées et excluant les diagnostics
Partiel : adb-files dispose de nombreuses entrées FILE_NATURE (DPE_ENERGY, DPE_GLOBAL, ASBESTOS, SERPULA, NATURAL_RISK = État des risques & pollutions, ELECTRICITY_INSTALLATION, GAS_INSTALLATION, DDT, CARREZ, BOUTIN) et FileMetaData porte un intervalle lifePeriod, si bien que les dates de validité peuvent être stockées. Manquent : (1) RentalContractReadyToSignValidator ne vérifie jamais que les annexes obligatoires existent et sont en cours de validité à la signature ; (2) NotifyAboutExpiredCertificatesJob (adb-files .../job/) ne couvre que l'assurance/la chaudière/le ramonage, et son @Scheduled est commenté ('deactivated until further details', LCS-1426) ; (3) aucune FILE_NATURE pour l'audit énergétique. Les diagnostics plomb/élec/gaz/ERP expirés passent inaperçus.
L'outil enregistre un garant avec un simple montant, mais ne distingue pas caution simple et caution solidaire, ne connaît pas la garantie Visale, et ne produit pas l'acte de cautionnement : ce document est aujourd'hui rédigé à la main en dehors du logiciel. Rien n'empêche non plus de cumuler une caution et une assurance loyers impayés (GLI) sur le même bail, ce qui est interdit, ni de dépasser le plafond légal du dépôt de garantie (un mois en vide, deux en meublé). Le risque : des garanties inopposables en cas d'impayé et des clauses illégales dans les dossiers.
Modèle garant/caution minimal : pas de Visale, pas d'acte de cautionnement, pas de plafonds légaux
GuaranteeContract existe (adb/adb-model) mais sa GuaranteeCondition ne porte qu'un montant et renewalCoveredNumber ; la validation (CommonContractValidator.validateGuaranteeForRentalContract) vérifie seulement que l'état du contrat est SIGNED. Manquent : le type de garantie Visale/Action Logement, la distinction caution simple / solidaire, la génération de l'acte de cautionnement (aucun modèle dans adb-reports ni adb-ui/HTML-Template), l'interdiction de cumul caution + GLI sur un même bail (loi Boutin art. 22-1), et aucun plafond sur le dépôt de garantie (1 mois vide / 2 meublé) — rentDepositAmount n'est vérifié que pour sa nullité/cohérence de bénéficiaire dans RentalContractReadyToSignValidator.
La colocation existe dans l'outil (type de dossier dédié, assurance des colocataires, clause de solidarité), mais la clause de solidarité n'est qu'un texte libre : l'outil ne distingue pas bail unique solidaire et baux individuels par chambre, ne suit pas les dates d'entrée et de sortie de chaque colocataire, et ne fait pas respecter la limite légale de six mois après le départ (loi ALUR), ni la libération du garant qui va avec. Pas non plus de répartition du loyer ni de quittance par colocataire. Aujourd'hui, tout cela se gère de tête ou sur tableur ; continuer à réclamer un loyer à un colocataire parti depuis plus de six mois est illégal et source de contentieux.
Prise en charge de la colocation superficielle : pas de baux multiples, pas de limite de solidarité à 6 mois
Partiel : adb-persons possède un type de personne FLATSHARE avec cotenantsData (PersonServiceImpl.patchFlatshare), l'UI a des onglets colocation et un sélecteur de type de locataire, et RentalContract porte cotenantInsurance/cotenantInsuranceAnnualAmount ainsi qu'une solidarityClause en texte libre (String). Manquent : le choix structuré entre bail unique solidaire et baux multiples par chambre ; le suivi des dates d'entrée/départ individuelles de chaque colocataire ; l'application du plafond ALUR de 6 mois après le départ pour la clause de solidarité (et la libération du garant associé) ; la répartition du loyer et la quittance par colocataire. L'exposition des colocataires sortants ne peut être calculée.
Le bail mobilité (1 à 10 mois, meublé, sans dépôt de garantie, sans reconduction tacite), destiné aux étudiants et salariés en mobilité, ne figure pas parmi les types de contrats proposés. Pour loger ce public, un gestionnaire doit aujourd'hui détourner le bail précaire, sans aucun des garde-fous propres au bail mobilité : rien n'empêche de demander un dépôt de garantie interdit ou de dépasser la durée maximale. Ajouter ce type de bail permettrait de servir ce marché en toute conformité, avec les bonnes règles appliquées automatiquement.
Absence de catégorie de contrat bail mobilité
Le catalogue RENTAL_CONTRACT_CATEGORY ne propose que : 1948, CIVIL, COMMERCIAL, DEROGATION (bail dérogatoire), EMPTY_RESIDENTIAL, FURNISHED_RESIDENTIAL, TEMPORARY (bail précaire). Le bail mobilité issu de la loi ELAN (1 à 10 mois, meublé, sans dépôt de garantie, sans reconduction tacite, locataires éligibles Visale) est absent du catalogue, du modèle et des validateurs ; ses contraintes spécifiques (bornes de durée, interdiction de dépôt, prolongation unique non renouvelable) ne peuvent donc être représentées — un gestionnaire devrait détourner le 'bail précaire' sans aucun des garde-fous. Pertinent pour les portefeuilles institutionnels logeant étudiants et travailleurs mobiles.
Le document envoyé chaque mois combine avis d'échéance et quittance de loyer : la quittance part donc en même temps que l'appel de loyer, que le locataire ait payé ou non. Or la loi réserve la quittance aux loyers effectivement encaissés (un simple reçu en cas de paiement partiel), et l'outil n'offre ni contrôle du paiement avant émission, ni quittance à la demande. Concrètement, un locataire en impayé détient un document où le bailleur atteste avoir été payé — un vrai handicap en cas de recouvrement ou de contentieux. Pour une quittance conforme, il faut aujourd'hui la produire à la main hors outil.
La quittance de loyer est émise dans le cadre de l'avis d'échéance, sans conditionnement au paiement ni délivrance à la demande
Partiel : le PDF d'appel de loyer (adb-reports/.../templates/call-for-rent.html) est intitulé 'Quittance de loyer et avis d'échéance' et contient la mention légale standard de quittance, et le bail capture rentReceiptSendingMethod (validé au ready-to-sign). Manquent : la section quittance est produite avec l'appel de loyer, c'est-à-dire avant/indépendamment du paiement — aucun code dans adb-reports ni adb-accounting ne conditionne l'émission de la quittance au règlement intégral de la période (art. 21 loi 89-462 : quittance uniquement pour le loyer payé, reçu en cas de paiement partiel), et il n'existe aucun endpoint de quittance autonome à la demande ni document reçu par paiement.
La facturation des honoraires de location (visite, dossier, état des lieux) fonctionne bien côté comptable, mais l'outil ne calcule ni ne vérifie jamais les plafonds légaux au mètre carré dus par le locataire (12, 10 ou 8 euros selon la zone, plus 3 euros pour l'état des lieux), d'autant qu'il ignore la zone de chaque bien. La section « plafonds applicables » du bail doit être remplie à la main, et le respect des montants repose entièrement sur la vigilance des équipes. Surfacturer le locataire est une pratique sanctionnée par la répression des fraudes : un contrôle automatique en fonction de la surface et de la zone supprimerait ce risque.
Absence de contrôle des plafonds d'honoraires de location (plafonds €/m² du décret 2014-890)
Le moteur d'honoraires est complet sur le plan comptable (honoraires RENTING_APPLICATION/INVENTORY/MEDIATION, comptes 622600/622610/622620, CallForRentFeesInvoiceCreationStrategy) et le modèle de bail imprime une section statique 'Plafonds applicables' pour les plafonds d'honoraires de visite/dossier/EDL (RC.html lignes 1796-1809) à saisir manuellement. Mais aucun code ne calcule ni ne valide les plafonds d'honoraires à la charge du locataire (12/10/8 €/m² selon la zone + 3 €/m² pour l'EDL, part locataire ≤ part propriétaire) au regard de la surface et de la zone du logement — de toute façon impossible sans référentiel de zones (cf. lacune zone tendue). La surfacturation des locataires est une pratique réprimée (DGCCRF).
Le circuit de relance ne connaît que trois niveaux de lettres. La mise en demeure et le commandement de payer n'existent que comme documents classés ou lignes de refacturation, sans étape de suivi : pas de suivi des délais de la clause résolutoire (six semaines pour le locataire, deux mois pour la CCAPEX depuis la loi anti-squat de 2023), pas de signalement CAF ou CCAPEX. Au-delà de la troisième relance, tout le précontentieux — pourtant indispensable pour activer une garantie loyers impayés (GLI) — se pilote hors outil, sur tableur ou chez l'huissier, avec le risque de rater un délai légal et de compromettre l'indemnisation.
La relance s'arrête à 3 lettres de rappel ; aucune étape juridique pré-contentieuse
adb-contracts possède un modèle de relance (Dunning/DunningNotice/DunningLevel, DunningFunction) mais le catalogue DUNNING_LEVEL ne contient que LEVEL_1/2/3 ('Relance 1/2/3'). La mise en demeure et le commandement de payer n'existent que comme natures de fichier passives (FORMAL_NOTICE, et l'événement comptable de refacturation 706645 'commandement'), sans étape de workflow, sans suivi du calendrier de la clause résolutoire (délais de notification de 6 semaines au locataire / 2 mois à la CCAPEX au titre de la loi anti-squat 2023), ni signalement CAF/CCAPEX. Pour un gestionnaire institutionnel gérant des sinistres GLI (les événements GLI existent en comptabilité), la chaîne pré-contentieuse est manuelle et non suivie.
Le système comptable en production ne sait pas générer le FEC, le fichier des écritures comptables que l'administration fiscale exige lors de tout contrôle. Les seuls exports disponibles sont des extractions vers Google Sheets ou Excel, qui ne respectent pas le format légal ; une ébauche existe dans la nouvelle version du logiciel, mais celle-ci n'est pas en service et reste incomplète. En cas de contrôle, l'entreprise ne pourrait pas remettre le fichier demandé, ce qui l'expose à une amende et à un rejet de comptabilité. Ce chantier doit être finalisé avant tout contrôle fiscal.
L'export FEC est absent de la pile comptable de production
Aucun générateur de FEC (Fichier des Écritures Comptables, art. L47 A-I LPF) n'existe dans le adb-accounting Java de production ni dans adb-ui. La seule mécanique d'export est le dump d'écritures Google Sheets/xlsx (SpreadSheetService, export-gl-modal). Un générateur de FEC existe uniquement dans le portage Cloudflare Workers NON en production (services/accounting/src/posting/fec.mjs, GET /accounting/entries/fec), et même là EcritureLet est laissé en dur à blanc et le EcritureNum continu n'est pas résolu. Tout contrôle fiscal d'un gestionnaire institutionnel exigera un FEC ; aujourd'hui le système de production ne peut en produire.
L'outil sait ouvrir un exercice comptable mais jamais le fermer : pas d'écritures de clôture, pas de report à nouveau, pas de verrouillage des périodes passées, pas de scellement ni d'archivage des écritures. Les écritures d'exercices anciens restent donc modifiables indéfiniment, ce qui rend impossible le respect des règles anti-fraude françaises (inaltérabilité, sécurisation, conservation, archivage) comme d'une clôture d'exercice normale. Aujourd'hui, la clôture ne se fait tout simplement pas dans l'outil ; en cas de contrôle fiscal ou d'audit du garant, l'agence ne peut pas prouver que ses comptes n'ont pas été retouchés après coup.
Aucun mécanisme de clôture d'exercice, de verrouillage de période ou d'inaltérabilité
FiscalYearEntity a un booléen `open`, et le pipeline d'écritures (FiscalYearHandler) exige seulement un exercice ouvert à la date d'écriture — mais rien ne clôture jamais un exercice : FiscalYearController n'expose que GET et POST, FiscalYearService n'a pas de méthode de clôture, et il n'existe ni écritures de clôture, ni report des à-nouveaux, ni verrouillages de période, ni hachage/scellement des écritures, ni archivage. Les règles anti-fraude françaises (art. 286-I-3° bis CGI : inaltérabilité, sécurisation, conservation, archivage) et la clôture d'exercice normale sont impossibles à implémenter ; les écritures comptabilisées restent modifiables dans le temps indéfiniment.
Le numéro de mandat est un texte libre saisi à la main lors de la finalisation de l'offre : rien ne garantit qu'il soit unique, chronologique et sans trou, et il n'existe ni registre inaltérable ni export du registre, contrairement à ce qu'exige la loi Hoguet. L'écran actuel n'est qu'une liste filtrable, pas un registre légal. L'enjeu est lourd : un mandat mal numéroté peut être annulé en justice, et avec lui le droit de percevoir les honoraires de gestion correspondants. Un vrai registre numéroterait automatiquement chaque mandat et produirait le document exigible en cas de contrôle.
Absence de registre des mandats (loi Hoguet, numérotation séquentielle)
Le numéro de mandat sur RentalDelegateContract est `easyId`, une valeur en texte libre saisie manuellement à la finalisation de l'offre (ContractStateServiceImpl.updateEasyIdIfApplicable, adb-contracts ligne ~336) ; adb-ui le mappe vers mandateNumber avec un repli sur '-'. Aucun service de numérotation chronologique séquentielle, aucun contrôle d'unicité/continuité, aucun registre immuable, aucun export du registre tel qu'exigé par les art. 72/65 du décret 72-678 (registre des mandats à numérotation continue). L'écran de liste des mandats de gestion est un tableau filtrable, pas un registre légal. Exposition : nullité des mandats et honoraires de gestion juridiquement irrécouvrables.
L'outil enregistre bien la carte professionnelle et la garantie financière de l'agence (organisme, montant, échéances), mais uniquement à titre déclaratif. Il ne calcule jamais le total des fonds mandants réellement détenus pour le comparer au plafond garanti, ne produit pas la pointe de gestion périodique attendue par le garant (Galian, CEGC…), et n'alerte ni en cas de dépassement ni à l'approche de l'expiration de la carte ou de la garantie. Ce contrôle, obligatoire au titre de la loi Hoguet, se fait donc aujourd'hui à la main sur tableur — s'il se fait ; un dépassement non détecté expose l'agence à un refus de couverture du garant et à une infraction réglementaire.
Absence de pointe de gestion : le plafond de garantie n'est jamais comparé aux fonds mandants détenus
Les paramètres de l'agence stockent la carte professionnelle (CCI, numéro, échéance) et les garanties financières (organisme, n° client, n° contrat, montant de la garantie) avec dépôt de document — données purement déclaratives. Rien ne calcule le total des fonds mandants détenus (solde bancaire 512101 / agrégats 466100+411100) au regard du plafond de garantie, aucun rapport périodique de pointe de gestion pour le garant (Galian/CEGC), aucune alerte lorsque les fonds dépassent la garantie ou lorsque la carte/garantie expire. Pour un portefeuille institutionnel, cette obligation Hoguet (art. 3 loi 70-9) est un contrôle opérationnel impératif.
La documentation prévoyait un import de fichiers bancaires avec rapprochement automatique, mais rien n'a été construit : chaque encaissement est pointé et saisi à la main, hors prélèvements automatiques. Par ailleurs, l'outil ne connaît qu'un seul compte bancaire par organisation, sans distinction entre le compte séparé des fonds des propriétaires mandants et les fonds propres du cabinet, et sans écran de rapprochement bancaire. L'intégrité des fonds détenus pour le compte de tiers repose donc entièrement sur la rigueur de la saisie manuelle, ce qui est fragile à grande échelle.
Absence d'import/rapprochement de relevés bancaires et de gestion d'un compte bancaire mandants séparé
La documentation d'architecture (adb-doc/docs/architecture/audit.md, séquence camt.054) décrit un 'import fichier bancaire' + auto-rapprochement, mais aucun code n'existe : aucun parseur ou endpoint d'import camt/CFONB/relevé où que ce soit dans adb-accounting (liste des contrôleurs vérifiée) ; les paiements sont saisis manuellement ou via le flux de prélèvement SEPA. Un seul compte bancaire existe — la ligne coa.csv 512101 'Banque Test' par organisation — sans modèle multi-comptes pour séparer le compte séparé mandants des fonds propres de l'agence, et sans écran de rapprochement bancaire. L'intégrité de la trésorerie des fonds de tiers repose entièrement sur la saisie manuelle.
Le relevé fiscal annuel par propriétaire — loyers encaissés, charges récupérables et déductibles, travaux, honoraires, assurances, présenté pour alimenter les déclarations 2044 ou 2072 — n'existe pas dans l'outil. Les comptes rendus de gestion (CRG) couvrent les mouvements de trésorerie période par période, mais rien n'agrège l'exercice fiscal par propriétaire selon les catégories des revenus fonciers, et aucun modèle de document n'existe pour cela. Chaque début d'année, les équipes doivent donc reconstituer ces relevés à la main à partir des CRG, un travail lourd et source d'erreurs sur un livrable que tout administrateur de biens fournit en standard.
Absence de relevé annuel de déclaration des revenus fonciers pour le propriétaire
Le livrable standard du gestionnaire immobilier français — un relevé annuel par propriétaire des loyers encaissés, charges récupérables/déductibles, travaux, honoraires et assurances, formaté pour alimenter les déclarations 2044/2072 — n'existe pas. Le CRG (CRGController, CRGServiceImpl, composant UI crg) couvre la reddition périodique des mouvements de trésorerie, et RentalAllowanceStatement couvre les allocations logement des locataires, mais il n'y a aucune agrégation par exercice et par propriétaire mappée aux catégories de revenus fonciers, aucun export 2044/2072, aucun modèle dans adb-reports (répertoire templates : uniquement RC, RDC, call-for-rent, resumption-of-lease).
L'outil calcule bien la TVA sur les factures d'honoraires, mais ne produit aucun état préparatoire à la déclaration mensuelle (CA3) : pas de total de TVA collectée et déductible par période, pas de paramétrage du régime. Surtout, la TVA est comptabilisée au moment de la facture, alors que pour des prestations de gestion elle est en principe due à l'encaissement : l'outil ne suit pas ce décalage. Aujourd'hui, le service comptable reconstruit chaque déclaration à la main à partir des exports du grand livre, avec un risque d'erreur de période et de redressement.
Absence de gestion de la déclaration de TVA (CA3) et de la TVA sur les encaissements
La plomberie TVA existe : comptes 445510/445660/445710 dans coa.csv, TaxCalculator (HT/TTC/vatAmount), TVA sur les factures d'honoraires. Mais il n'y a aucune préparation de CA3 : aucun rapport totalisant la TVA collectée/déductible par période de déclaration, aucune configuration de régime de TVA et, surtout, aucune logique de TVA sur les encaissements — la TVA est comptabilisée à la date de la facture/de l'événement sans suivi de l'exigibilité au paiement, alors que les honoraires de gestion sont des prestations de services pour lesquelles la TVA est due à l'encaissement en l'absence d'option pour les débits. Un gestionnaire institutionnel doit reconstituer les chiffres de la CA3 manuellement à partir des exports du grand livre.
Toutes les données existent — fournisseurs, factures, règlements, comptes d'honoraires — mais l'outil ne produit aucune extraction DAS2 : pas d'état annuel des honoraires, commissions et courtages versés par bénéficiaire au-delà du seuil de 1 200 €, pas de saisie de SIRET et d'adresse orientée déclaration, pas de fichier d'export. Or une agence qui paie avocats, huissiers ou diagnostiqueurs est légalement tenue de déposer une DAS2 chaque année. Aujourd'hui, la déclaration doit être reconstituée à la main depuis la comptabilité ; l'omission expose à une amende fiscale proportionnelle aux sommes non déclarées.
Absence de déclaration DAS2 des honoraires versés à des tiers
Les fournisseurs et honoraires sont entièrement suivis (comptes auxiliaires 401100/SUP, INCOMING_SUPPLIER_INVOICE, SUPPLIER_PAYMENT_ISSUED, comptes d'honoraires 6226xx), les données existent donc, mais il n'y a aucune extraction DAS2 annuelle (art. 240 CGI) : aucun état des honoraires/commissions/courtages versés par bénéficiaire au-delà du seuil de 1 200 €, aucune capture SIRET/adresse adaptée à la déclaration, aucun fichier d'export. Les gestionnaires institutionnels rémunérant avocats, huissiers (les événements commandement/sommation existent : 706640/706645) et diagnostiqueurs sont légalement tenus de déposer une DAS2 chaque année.
Les briques unitaires existent : l'outil sait comptabiliser la taxe foncière, refacturer la taxe d'ordures ménagères à un locataire et archiver l'avis d'imposition. Mais il n'y a aucun outil de campagne : à réception des avis annuels, la refacturation au prorata d'occupation doit être saisie contrat par contrat, ce qui est ingérable sur un parc institutionnel. En pratique, une partie de ces refacturations risque d'être oubliée ou faite en retard, soit du chiffre récupérable perdu. Un traitement en masse (saisie de l'avis par immeuble, ventilation automatique sur tous les locataires en place) réglerait le problème.
Absence d'outillage de campagne annuelle taxe foncière / TEOM
La mécanique au niveau du lot existe : variantes CFR_CHARGES_HOUSEHOLD_WASTE et ONE_TIME, ajustement HOUSEHOLD_WASTE_TAX_DUE_BY_TENANT, comptes 635120 (taxe foncière), refacturation 706630/706635, FILE_NATURE.PROPERTY_TAX pour stocker l'avis. Manque : tout workflow de campagne pour saisir l'avis de taxe foncière annuel par bien et déployer en masse la refacturation prorata de la TEOM à tous les locataires en place (avec prorata d'occupation) — chaque contrat doit être ajusté un par un, ingérable à l'échelle institutionnelle. Connexe : RENTAL_CHARGES_ADJUSTMENT (régularisation des charges) est le seul événement encore marqué 'En cours' (non livré).
L'outil sait générer les comptes rendus de gestion (CRG), mais la périodicité convenue avec chaque propriétaire n'est enregistrée nulle part dans le mandat, et aucun mécanisme ne déclenche automatiquement les CRG ni les reversements. Le décret de la loi Hoguet impose pourtant de rendre compte à la fréquence prévue au mandat. Aujourd'hui, la conformité tient à un gestionnaire qui pense à lancer chaque période à la main, sans aucun suivi des mandats en retard : un propriétaire oublié ne se voit ni rendre compte, ni reverser ses fonds dans les temps.
La reddition des comptes (CRG) n'a pas de périodicité ni de planification pilotées par le contrat
La génération du CRG existe (CRGController, CRGServiceImpl, CRGPeriod avec upFrom/downTo, composant UI crg d'adb-ui, paiement propriétaire selon le cas de gestion 'périodicité du CRG') mais la périodicité n'est modélisée nulle part : RentalDelegateContract n'a aucun champ de fréquence de CRG, et aucun ordonnanceur ne déclenche les traitements CRG/paiement propriétaire (seul le cron ContractRenewalJobExecutor existe). Le décret 72-678 impose de rendre compte à la périodicité convenue au mandat ; aujourd'hui la conformité dépend d'un opérateur pensant à lancer chaque période manuellement, sans suivi des retards par mandat.
Le moteur comptable est complet (journaux, plan de comptes, numérotation des pièces) et le format légal du FEC est même documenté en interne, mais aucun code d'export n'existe : le plus proche est une extraction vers Google Sheets ou Excel. Il n'y a pas non plus de fichier d'échange vers Sage, Cegid ou Quadra, alors que les propriétaires institutionnels attendent d'alimenter leur propre comptabilité. En cas de contrôle fiscal, l'entreprise serait dans l'incapacité de remettre le fichier obligatoire ; au quotidien, toute transmission de données comptables aux clients passe par des retraitements manuels.
Absence d'export FEC ou d'interface comptable (Sage/Cegid) alors que le FEC est un objectif de conformité affiché
Le moteur de grand livre, les 13 journaux, les comptes du PCG et les chronos de références de pièces existent tous dans adb-accounting, et adb-doc/docs/concepts/accounting/fec.md spécifie les 19 champs du FEC — mais aucun code d'export n'existe. La mécanique la plus proche est l'export d'écritures Google Sheets/xlsx (SpreadSheetService/GoogleSpreadSheetServiceImpl). Aucune interface fichier plat Sage/Cegid/Quadra non plus. Lors de tout contrôle fiscal (LPF art. L47 A), une comptabilité informatisée doit produire un FEC conforme ; les propriétaires institutionnels attendent aussi des flux comptables vers leurs propres ERP. Les questions ouvertes de la doc (EcritureNum continu, champs de lettrage, références de pièces externes) restent non implémentées.
L'outil connaît les charges d'immeuble, leur caractère récupérable et les clés de répartition, mais rien ne calcule l'écart entre les provisions facturées et les charges réelles récupérables de chaque locataire, et il n'existe aucune campagne annuelle en masse avec simulation préalable ou possibilité d'annulation. La seule voie est une écriture manuelle par locataire. La régularisation annuelle est pourtant une obligation légale (avec une prescription de trois ans pour récupérer les sommes) ; à l'échelle d'un portefeuille institutionnel, la faire à la main est ingérable, et les charges non régularisées à temps sont définitivement perdues pour les propriétaires.
Régularisation des charges : aucun moteur de calcul ni campagne de masse — uniquement des saisies manuelles par locataire
RENTAL_CHARGES_ADJUSTMENT est le seul événement non livré ('En cours') ; dans le code il n'est accessible que comme nature de ManualEntryEvent (adb-accounting/.../model/events/ManualEntryEvent.java). adb-parts détient les charges d'immeuble (ChargeEntity, drapeau isRecoverable) avec clés de répartition et budgets, mais rien ne calcule les provisions appelées (706200) par rapport aux charges récupérables réelles par locataire, et il n'existe aucun traitement annuel en masse avec aperçu/simulation ou annulation. La régularisation annuelle des charges est une obligation légale (loi 89-462 art. 23, limite de récupération de 3 ans) ; la faire manuellement par locataire est ingérable à l'échelle institutionnelle.
L'outil sait générer les fichiers de prélèvement des loyers auprès des locataires, mais pas les fichiers de virement dans l'autre sens : les reversements mensuels aux propriétaires après clôture des comptes-rendus de gérance, et les règlements fournisseurs. Chaque mois, ces virements — potentiellement des milliers — doivent être saisis un par un dans le portail bancaire, puis ressaisis dans la comptabilité. Cela double la charge de travail et crée des écarts de rapprochement sur le compte des fonds mandants. La génération automatique du fichier de virements supprimerait la double saisie et les erreurs associées.
Absence de génération de virements SEPA (pain.001) pour les reversements propriétaires et les paiements fournisseurs
La prise en charge SEPA ne couvre que les prélèvements : DirectDebitExportService et AbstractDirectDebitXmlMapper d'adb-contracts (CustomerDirectDebitInitiationV02 = collectes pain.008 auprès des locataires). Il n'existe aucun lot pain.001 pour OWNER_PAYMENT_ISSUED / SUPPLIER_PAYMENT_ISSUED — le traitement mensuel de reversement aux propriétaires (des milliers de propriétaires après clôture du CRG) doit être saisi manuellement dans le portail bancaire puis ressaisi en événements comptables, doublant la charge de travail et créant une dérive de rapprochement sur le compte mandants.
Les droits d'accès distinguent lecture et écriture, mais pas préparateur et valideur : la même habilitation permet à la fois de proposer un paiement et de l'exécuter, qu'il s'agisse d'un reversement propriétaire ou d'un règlement fournisseur. Il n'existe ni rôle d'approbateur, ni seuil au-delà duquel une seconde signature serait exigée, ni étape de validation des lots de paiement. L'outil trace bien qui a fait quoi, mais après coup seulement : la séparation des tâches — contrôle standard contre la fraude interne et l'erreur, attendu par tout auditeur d'un gestionnaire institutionnel — repose aujourd'hui uniquement sur l'organisation interne, sans garde-fou dans l'outil.
Absence de workflow de validation ou de séparation des tâches sur les paiements sortants
L'autorisation repose sur des scopes OAuth grossiers (accounting.read/write/delete dans SecurityConfig d'adb-accounting) ; le même scope accounting.write propose et traite les paiements — la dualité proposer/traiter est un mécanisme de brouillon, pas un contrôle maker-checker. Les droits UI (rights-enum.ts) ont des paires lecture-écriture Financial/Accounting mais aucun rôle d'approbateur/valideur, aucun seuil à quatre yeux, aucune étape de validation de lot de paiements. Les journaux d'action (adb-action-logs : userId, method, path, target) donnent la traçabilité de qui a agi, mais rien n'impose qu'une seconde personne valide un reversement propriétaire ou un paiement fournisseur — un contrôle standard institutionnel et de garantie financière.
Aucun import de relevés bancaires ni module de rapprochement n'existe : la fonction n'apparaît que dans la documentation, jamais dans le logiciel. Tous les paiements entrants hors prélèvement automatique sont saisis manuellement, et personne ne peut vérifier systématiquement que la banque en comptabilité correspond au vrai compte séquestre. Pour un cabinet qui détient les fonds de ses mandants, ce rapprochement périodique est un contrôle fondamental attendu par le garant financier et les auditeurs. Au volume d'un parc institutionnel, la gestion des paiements non identifiés sans import de relevés ne tient pas.
Absence d'import de relevés bancaires ou de rapprochement bancaire
Aucune ingestion de relevé bancaire camt.053/OFX/CSV et aucun module de rapprochement où que ce soit ; les seules occurrences sont des mentions en prose dans adb-doc (architecture/audit.md, services/adb-accounting.md). Tous les paiements entrants sont saisis manuellement (ManualPaymentCreationStrategy) ou déduits des traitements de prélèvement. Pour un gestionnaire détenant des fonds mandants, le rapprochement périodique du 512101 avec le compte bancaire séquestre est un contrôle essentiel (garantie financière loi Hoguet, audits de cabinet) ; à volume institutionnel, la gestion des paiements non rapprochés sans import de relevé ne passe pas à l'échelle.
L'outil calcule des soldes de comptes à un instant donné et gère les relances contrat par contrat, mais il n'offre aucune vue d'ensemble : pas de balance âgée des créances (30, 60, 90 jours), pas de délai moyen d'encaissement, pas de tableau de bord des impayés du portefeuille, pas de provisionnement des locataires douteux hors sinistres GLI, et pas d'indicateurs de vacance ou de délai de relocation. Les écrans de tableau de bord existants sont des coquilles sans ces indicateurs. La direction pilote donc aujourd'hui les impayés et la vacance sur tableur, à partir d'extractions manuelles — avec le risque de découvrir tard une dérive du portefeuille.
Absence de balance âgée, de DSO ou d'indicateurs de portefeuille sur les impayés/la vacance
BalanceService/BalanceAggregationOperations calculent des soldes de compte à un instant donné, la relance existe par contrat (Dunning/DunningNotice/DunningLevel), et un compte de créances douteuses (416100) existe mais n'est mouvementé que par les événements GLI. Manquent : les tranches d'antériorité des créances (30/60/90), le DSO, les tableaux de bord d'impayés du portefeuille, le provisionnement des locataires douteux en dehors de la GLI, et les indicateurs de taux de vacance/délai de relocation (la vacance n'est qu'un tag de ticket avec date du dernier événement sur UnitAggregate). Le module tableau de bord de l'UI (dashboard-model.ts, graphiques par baseUrl) et le Global Rental Report sont des coquilles de listes/graphiques dépourvues de ces indicateurs institutionnels.
Le calcul de la révision IRL est correct contrat par contrat, mais il se déclenche automatiquement, sans intervention humaine, dès la publication de l'indice ou au renouvellement. Il n'existe aucun écran de campagne à l'échelle du portefeuille : impossible de simuler les nouveaux loyers avant application, de lister les cas particuliers (indice manquant, loyer gelé pour cause de passoire énergétique, plafonnement), de valider en masse, ou d'annuler une révision erronée. Sur des milliers de baux, une anomalie de calcul peut donc passer inaperçue jusqu'aux réclamations des locataires ou des propriétaires.
Absence d'outillage de campagne d'indexation de masse (aperçu, rapport d'exceptions, annulation) pour la révision IRL annuelle
Le calcul d'indexation et les règles d'éligibilité existent par contrat (adb-model RentalContract implémente RentAmountIndexer ; rule/impl/indexation, plafond de pouvoir d'achat, règles de gel DPE), déclenchés silencieusement par le cron de renouvellement (ContractRenewalJobExecutor) et la publication de l'indice INSEE (IndexFunction SQS). Le seul point de contact UI est un composant mono-contrat (rc-conditions). Manque, à l'échelle institutionnelle : un écran de campagne au niveau du portefeuille — simuler/prévisualiser les nouveaux loyers, lister les exceptions (indice manquant, plafonné par l'encadrement/le DPE, rétroactivité), approuver en masse et annuler un traitement erroné. L'application automatique et silencieuse sur des milliers de baux est un risque opérationnel et de notification légale.
Les mandats de gestion enregistrent bien leur durée et le barème complet des honoraires, mais rien ne surveille leur échéance : pas de tableau de bord des mandats arrivant à terme, pas de rappel avant la fin, et le seul automatisme existant concerne les révisions de loyer, pas les mandats. Le suivi des renouvellements se fait donc aujourd'hui sur tableur ou de mémoire. Or, au regard de la loi Hoguet, gérer et facturer des honoraires sur un mandat expiré est sans base légale — un constat classique d'audit, avec à la clé des honoraires à rembourser.
Absence d'alertes d'échéance de mandat de gestion ou de pipeline de renouvellement
RentalDelegateContract porte une durée et un barème d'honoraires complet par mandat (honoraires de gestion % + assiette, honoraires application/inventory/mediation — cette partie est présente), mais le cron de renouvellement ne vise que les dates de renouvellement de loyer à reconduction tacite, et le module 'notifications' d'adb-utilities correspond à des bandeaux admin (banner-dto.ts), pas à des alertes métier. Aucun tableau de bord d'échéances, aucun rappel avant la fin du mandat. Sous la loi Hoguet, gérer et facturer des honoraires sur un mandat expiré n'a aucune base légale et constitue une observation d'audit classique pour les mandats institutionnels.
Un vrai module de budgets par immeuble existe (lignes budgétaires, exercices, clés de répartition), mais il est coupé de la comptabilité : aucun événement comptable ne constate de provision pour travaux, il n'existe pas d'appel de provision travaux auprès des propriétaires, pas de compte de réserve dédié, et le compte-rendu de gérance ne compare pas le budget au réalisé. Les propriétaires institutionnels attendent pourtant un budget travaux approuvé par immeuble, des appels de provision et un suivi des dépenses par rapport à ce budget. Aujourd'hui, ce suivi se fait forcément hors outil, sur tableur.
Les budgets existent dans adb-parts mais aucune provision pour travaux n'est reliée à la comptabilité propriétaire
adb-parts dispose d'un vrai domaine budgétaire (BudgetLine, BudgetYear, MonthlyAmount, états, clés de répartition) et de charges d'immeuble, mais il est déconnecté du moteur comptable : aucun des 67 événements ne comptabilise de provisions pour travaux, il n'y a aucun 'appel de provision travaux' aux propriétaires, aucun compte de provision/réserve dans le plan comptable condensé, et aucun rapport budget-vs-réalisé propriétaire dans le CRG. Les propriétaires institutionnels attendent un budget travaux approuvé par immeuble, des appels de provision et un suivi des dépenses d'investissement par rapport à celui-ci.
Les versements d'aides au logement de la CAF sont bien gérés, mais tout le reste du logement conventionné manque : aucune donnée de convention APL sur les biens ou les baux, aucun calcul du supplément de loyer de solidarité (comparaison des revenus aux plafonds, enquête auprès des occupants), aucune facturation de ce supplément. Sans conséquence pour un portefeuille entièrement en secteur libre, ce manque est bloquant dès qu'un seul logement conventionné ou social est géré : ces obligations devraient alors être tenues intégralement hors outil.
Absence de prise en charge du SLS (supplément de loyer de solidarité) ou du conventionnement (convention APL)
Les versements d'allocation CAF/APL sont pris en charge (service/contrôleur RentalAllowanceStatement dans adb-accounting, tenantAllowanceId/allowanceAmount sur ContractEntity), les locataires sociaux entrent donc partiellement dans le périmètre — mais il n'y a aucun modèle de données de convention APL sur les lots/contrats, aucun calcul du SLS (revenus vs plafonds, workflow d'enquête OPS), et aucun événement de facturation du SLS. Les seules occurrences de 'solidarité' sont la clause de solidarité entre colocataires. Sans objet pour les portefeuilles du secteur libre ; bloquant si du logement conventionné/social est géré.
Le compte banque n'est alimenté que par des saisies manuelles et par les fichiers de prélèvement : il n'existe aucun import de relevés (quel qu'en soit le format), aucun moteur de pointage, aucun écran de rapprochement, aucun module de remise de chèques, ni de bannette pour les fonds non identifiés. Le parcours utilisateur documenté prévoit pourtant une étape « rapprocher le fichier bancaire ». Pour un cabinet qui détient les fonds de ses mandants sur un compte commun, cela signifie que le solde bancaire en comptabilité peut diverger du compte réel sans qu'aucun contrôle systématique ne le détecte.
Absence de rapprochement bancaire ou d'import de relevé bancaire
Le compte 512101 n'est alimenté que par des paiements saisis manuellement (ManualPaymentCreationStrategy, PaymentController) et les exports de prélèvement SEPA. Il n'y a aucun import de relevés bancaires (CFONB120, EBICS, camt.053/054, MT940), aucun moteur de rapprochement, aucune UI de rapprochement, aucun module de remise de chèques, ni aucun poste de travail pour les fonds non affectés. Le parcours utilisateur d'AUDIT.md liste 'Reconcile bank file' comme une étape, mais aucun code ne l'implémente. Pour une agence détenant des fonds mandants sur un compte mutualisé (loi Hoguet / garantie financière), le solde bancaire du grand livre peut silencieusement diverger du compte réel sans aucun contrôle systématique.
La loi impose chaque année une régularisation des charges locatives, réel contre provisions, accompagnée d'un décompte par nature de charges remis au locataire. Les briques existent — clés de répartition, budgets d'immeuble, une opération comptable prévue mais jamais finalisée — mais rien ne calcule les dépenses réelles ventilées par clé et par période d'occupation, rien ne les compare aux provisions facturées, et le décompte obligatoire n'est produit nulle part. Aujourd'hui, ce travail se fait sur tableur puis se saisit à la main écriture par écriture ; à l'échelle de milliers de lots, il est en pratique repoussé, au détriment des propriétaires et de la conformité.
Absence de moteur de régularisation annuelle des charges locatives (régularisation des charges)
La loi 89 art. 23 impose une régularisation annuelle réel-vs-provision par locataire. L'événement RENTAL_CHARGES_ADJUSTMENT existe dans AccountingEventType (doc : seul événement encore 'En cours') mais n'est accessible que comme écriture manuelle (adb-ui accounting-manual-entries, ManualEntryEvent). Des clés de répartition existent dans adb-utilities (allocationkeys/*) et des budgets d'immeuble dans adb-parts (BudgetServiceImpl), mais rien ne calcule dépenses réelles × clé de répartition × période d'occupation, ne compare aux provisions appelées (706200), ni ne produit le décompte obligatoire par nature de charges. À l'échelle institutionnelle (des milliers de baux), les écritures manuelles par locataire sont ingérables et juridiquement fragiles.
Les briques existent isolément : préavis, prorata du dernier loyer, calcul du délai légal de restitution du dépôt de garantie (un ou deux mois selon l'état des lieux) et écriture de remboursement du dépôt. Mais rien ne les enchaîne : la restitution du dépôt est une simple écriture comptable saisie à la main, sans étape pour le dernier appel proratisé, la régularisation des charges, les retenues justifiées pour réparations et la compensation avec le dépôt — et aucun document « arrêté des comptes » n'est produit pour le locataire. Chaque départ est donc reconstitué manuellement par le gestionnaire, avec un risque d'oubli de retenue, d'erreur de montant ou de dépassement du délai légal de restitution.
Absence d'arrêté des comptes de sortie locataire orchestré (solde de tout compte / arrêté des comptes)
Les briques existent isolément : états de préavis/résiliation dans RentalContractStateMachineConfig, calcul du loyer au prorata (RentalContract.proratedMonth), le délai légal de restitution (ContractAfterMapper.setEndContractSettlementTerm calcule inventoryDateOut + 1/2 mois selon la conformité de l'EDL), et TENANT_DEPOSIT_REIMBURSEMENT. Mais l'événement de remboursement n'est qu'une écriture comptable manuelle (adb-ui entry-details.component.ts) ; il n'y a aucun workflow enchaînant l'appel final au prorata, la régularisation des charges, les réparations déductibles (retenues justifiées), la compensation du dépôt de garantie, ni aucun document/modèle de 'solde de tout compte' pour le locataire sortant. L'automatisation d'AUDIT.md 'clôturer les factures en attente, rembourser le dépôt' n'a aucun code correspondant dans ContractsFunctions.
L'état des lieux se résume dans l'outil à des dates d'entrée et de sortie, un indicateur de conformité et un document scanné rangé dans le dossier. Il n'y a ni relevé structuré pièce par pièce, ni photos, ni comparaison entrée/sortie, ni grille de vétusté, et aucun lien entre les constats de sortie et les retenues sur le dépôt de garantie. L'indicateur de conformité ne sert qu'à fixer le délai de restitution (un ou deux mois). Les états des lieux se font donc aujourd'hui sur papier ou via un outil externe ; en cas de contestation d'une retenue, le gestionnaire doit reconstituer les preuves à la main, avec un risque réel de devoir restituer le dépôt faute de justification.
L'état des lieux n'existe que sous forme de dates + PDF scanné, sans EDL structuré
La prise en charge de l'EDL se limite à : inventoryDateIn/Out et inventoryOutCompliance sur le contrat (RentDates, composants rc-add-inventory-date / rc-confirm-inventory-modal), FILE_NATURE.CHECK_IN/CHECK_OUT pour les scans déposés, et les événements d'honoraires d'EDL. Il n'y a aucune saisie structurée de l'état pièce par pièce, aucune photo, aucune comparaison entrée/sortie, aucune grille de vétusté, et aucun lien entre les constats d'EDL et les retenues sur dépôt de garantie (REPAIRS_DUE_BY_TENANT est un ajustement manuel sans rapport). Le drapeau de conformité ne pilote que le délai de restitution (1 vs 2 mois). Les gestionnaires institutionnels doivent justifier chaque retenue sur dépôt par une preuve d'EDL ; aujourd'hui cela est entièrement hors du système.
La reprise d'un mandat venant d'un autre gestionnaire est bien couverte (reprise des soldes, journal dédié), mais le chemin inverse n'existe pas : quand un propriétaire confie son bien à un concurrent, il n'y a ni clôture de fin de mandat, ni arrêté final du compte-rendu de gérance, ni restitution organisée des fonds du propriétaire et des dépôts de garantie détenus, ni transfert des dossiers au successeur — seule une date de fin de mandat s'affiche à l'écran. Sur un parc institutionnel où les mandats tournent régulièrement, chaque sortie est aujourd'hui gérée à la main, avec un risque d'erreur sur les fonds restitués et un travail de reconstitution important.
La sortie de portefeuille propriétaire / le transfert en fin de mandat ne sont pas modélisés
L'onboarding depuis un précédent gestionnaire est bien couvert (7 événements ACCOUNTING_TAKEOVER_*, journal Reprise de soldes, TakeOverContractIndexGenerator). L'inverse — la perte d'un mandat au profit d'un autre gestionnaire — n'a rien : les événements de reprise sont entrants uniquement (côté propriétaire, seul OWNER_BALANCE_CREDITOR reçoit la trésorerie), il n'existe aucune famille d'événements de transfert de solde sortant, aucun workflow de fin de mandat (arrêté final du CRG, restitution des fonds propriétaires et des dépôts de garantie détenus, transfert des dossiers/documents au successeur), et seulement une colonne d'affichage MANDATE_END_DATE dans l'UI RDC. Les portefeuilles institutionnels font tourner leurs mandats régulièrement ; les sorties seraient des écritures manuelles improvisées.
L'outil génère les fichiers de prélèvement SEPA et enregistre le loyer comme encaissé dès l'envoi à la banque. Mais les retours de la banque ne sont jamais traités : aucun statut de rejet ou d'impayé, aucune contre-passation automatique de l'encaissement, aucune représentation du prélèvement ni refacturation des frais bancaires, et aucun basculement automatique vers la relance. Un loyer rejeté reste donc affiché comme payé jusqu'à ce que quelqu'un le repère sur le relevé bancaire et corrige tout à la main. Résultat : des relevés propriétaires faux, des relances qui partent en retard, et des impayés qui grossissent sans que personne ne les voie.
Absence de traitement des rejets de prélèvement SEPA (impayés / R-transactions)
adb-contracts exporte des fichiers pain.008 (DirectDebitExportServiceImpl, AbstractDirectDebitXmlMapper) et onDirectDebitProcessed comptabilise TENANT_PAYMENT_RECEIVED. Mais il n'y a aucun import ni traitement des fichiers de retour bancaire (rejets pain.002, camt.054) : aucun état de rejet/impayé où que ce soit dans le modèle directdebit, aucune contrepassation automatique du paiement sur RJCT/RTND, aucune re-présentation ni refacturation de frais bancaires, et aucune alimentation automatique du module de relance. Un prélèvement de loyer rejeté reste donc comptabilisé comme payé jusqu'à ce que quelqu'un le remarque sur le relevé bancaire — aggravant la lacune de rapprochement manquant et corrompant les soldes locataires et le CRG.
La recherche de locataire est suivie comme une simple frise d'étapes (candidature reçue, en validation, acceptée), pas comme de vrais dossiers : il n'existe ni fiche candidat avec revenus, ni calcul de solvabilité (règle des trois fois le loyer, taux d'effort), ni liste de pièces justificatives conforme à la réglementation, ni validation du dossier du garant, ni comparaison des candidatures. Les équipes constituent et évaluent donc les dossiers hors outil, par e-mail et tableur, ce qui ralentit la relocation — pourtant au cœur de la promesse faite aux propriétaires institutionnels de réduire la vacance — et affaiblit la traçabilité des choix en cas de contestation.
Le pipeline de relocation manque de dossiers de candidats et de contrôles de solvabilité
Des tickets de recherche de locataire existent (TenantSearchTicketDetails : rentAmount, chargeAmount, rentDepositAmount ; TICKET_EVENT_TYPE.APPLICATION_RECEIVED/UNDER_VALIDATION/ACCEPTED) — un fil d'étapes, pas un module de candidature. Il n'y a aucune entité candidat avec données de revenus, aucun calcul de solvabilité (ratio loyer × 3, taux d'effort), aucune checklist des pièces justificatives du décret 2015-1437, aucune validation du dossier de garant (éligibilité Visale/GLI), aucun scoring ni comparaison des candidats. adb-persons n'a de natures de fichier emploi/revenus (EMPLOYMENT_CONTRACT) qu'à titre de libellés de documents. Les workflows de réduction de la vacance (l'indicateur clé de relocation pour les propriétaires institutionnels) sont essentiellement suivis en texte libre.
L'envoi de documents fonctionne — avis d'échéance et quittances par courriel, comptes rendus de gestion en PDF — mais il n'existe aucun espace en ligne où un locataire consulterait ses quittances, son solde et ses paiements, ni où un propriétaire institutionnel retrouverait ses CRG, ses comptes et ses documents. Le suivi des envois en masse est par ailleurs quasi inexistant. Chaque demande de duplicata ou de justificatif passe donc par un gestionnaire qui recherche et renvoie le document à la main — une charge de travail permanente, et un vrai handicap commercial face aux mandants institutionnels qui attendent un accès en libre-service.
Absence de portail locataire/propriétaire ; la remise des documents se fait par e-mail uniquement, document par document
La distribution existe : adb-notifications (EmailSender SendGrid, CallForRentMessage) envoie les avis d'échéance, des mises en page de quittance existent (RENT_RECEIPT_SENDING_EMAIL, FILE_NATURE.RENT_RECEIPT avec un numéro de série de quittance), et les PDF de CRG sont générés dans adb-accounting. Manque : tout portail en libre-service pour les locataires (historique des quittances, solde, paiements) ou pour les propriétaires institutionnels (CRG, grands livres, documents) — les 'invitations' d'adb-ui n'invitent que des utilisateurs back-office ; adb-web n'est pas en production. Également aucun suivi de campagne/dispatch en masse (échecs d'envoi via FailedMessageHandler uniquement). Pour des propriétaires institutionnels attendant un accès extranet au reporting, c'est une faiblesse concurrentielle/contractuelle plutôt que légale.
Le premier passage d'audit craignait que le système décide trop mécaniquement du sens des reversements au propriétaire, et qu'une écriture manuelle passée au crédit puisse déclencher un versement indu au propriétaire. La contre-vérification a montré que ce scénario ne se produit pas : les écritures se compensent correctement et aucun crédit indu au propriétaire n'a pu être mis en évidence. Aucune action n'est nécessaire.
Détail —
Il était affirmé que lorsqu'un locataire rembourse un loyer déjà indemnisé par l'assurance loyers impayés (GLI), le propriétaire serait crédité une seconde fois en plus de l'indemnité déjà reversée. La contre-vérification a montré que les écritures se neutralisent : le propriétaire n'est pas payé deux fois. Constat infirmé, pas de risque financier avéré.
Détail —
Le premier passage signalait que plusieurs comptes de produits utilisés par le système n'apparaissaient pas dans la documentation de référence, et qu'un compte portait deux descriptions différentes. La contre-vérification a conclu que l'écart venait d'une documentation en retard sur la réalité, pas d'une erreur comptable : les écritures sont imputées de manière cohérente. C'est la documentation qui est à mettre à jour, pas la comptabilité.
Détail —
Il était affirmé que les comptes comptables utilisés pour la famille des loyers ne suivaient pas la matrice de référence, avec un compte réutilisé sous deux libellés différents. La contre-vérification a montré que la comptabilité réelle est cohérente et que l'écart tient à une documentation obsolète. Aucune écriture erronée n'a été trouvée.
Détail —
Lors d'une reprise de gestion, le système aurait toujours supposé que le dépôt de garantie était encaissé en banque, même quand c'est le propriétaire qui le détient, faussant la trésorerie affichée. La contre-vérification a montré que les écritures de reprise traitent correctement ce cas et que la crainte n'était pas fondée. Pas d'écart de trésorerie avéré.
Détail —
Le premier passage signalait qu'un même compte comptable était partagé par deux types de frais ponctuels différents, avec des libellés contradictoires. La contre-vérification a conclu que les imputations réelles sont correctes et que la contradiction venait d'une documentation dépassée. Aucune confusion comptable réelle n'a été constatée.
Détail —
Il était affirmé que si une annulation d'écriture était traitée deux fois (par exemple après un incident technique), les contre-passations seraient dupliquées et fausseraient les comptes. La contre-vérification a montré que des garde-fous empêchent cette duplication : retraiter une annulation ne double pas les écritures. Constat infirmé.
Détail —
Le premier passage affirmait qu'un remboursement reçu d'un fournisseur (avoir, trop-perçu) n'était jamais reversé au propriétaire concerné, qui aurait donc perdu cet argent sur son relevé. La contre-vérification a trouvé les écritures compensatoires que le premier passage avait manquées : le propriétaire est bien recrédité. Pas d'argent perdu pour les propriétaires.
Détail —
Il était affirmé que les honoraires en pourcentage déclenchés à la signature d'un contrat étaient calculés sur une base fictive de 1 euro, donc quasi nuls et faux. La contre-vérification a montré que les honoraires réellement facturés sont calculés correctement ; la valeur fictive relevée n'est pas celle utilisée pour la facturation. Aucun manque à gagner avéré.
Détail —
Le premier passage craignait que, dans certains dossiers GLI, un même paiement de locataire déclenche à la fois un reversement au propriétaire et un remboursement à l'assureur, soit une double sortie d'argent. La contre-vérification a montré que les écritures se compensent et qu'aucune double sortie ne se produit. Constat infirmé.
Détail —
Il était affirmé que le système ne permettait ni de suivre les attestations d'assurance habitation des locataires, ni de gérer les franchises et délais de carence de la GLI. La contre-vérification a montré que cette lecture était erronée : le besoin est couvert ailleurs dans le système et le manque annoncé n'est pas confirmé.
Détail —
Il était affirmé qu'un remboursement lié à la garantie loyers impayés créditait le propriétaire une seconde fois et laissait un déséquilibre dans les comptes de suivi par propriétaire. La contre-vérification a trouvé les écritures compensatoires manquées au premier passage : pas de double crédit, pas de déséquilibre. Constat infirmé.
Détail —
Le premier passage affirmait que la date de fin de bail était calculée à partir de la date de saisie plutôt que de la date réelle du congé, faussant la fin de contrat pour les congés enregistrés avec retard. La contre-vérification a montré que le calcul aboutit à la bonne date de fin : le scénario redouté ne se matérialise pas. Constat infirmé.
Détail —
Il était affirmé que lors d'une reprise de gestion, le dépôt de garantie était systématiquement enregistré comme encaissé en banque, faussant le compte bancaire ou rendant négatif le compte des dépôts quand c'est le propriétaire qui le détient. La contre-vérification a montré que les écritures de reprise gèrent correctement ce cas. Pas d'anomalie confirmée.
Détail —
Le premier passage signalait qu'une opération arrivant sans date comptable provoquerait une erreur technique bloquant le traitement des écritures. La contre-vérification a montré qu'un garde-fou en amont garantit que la date est toujours renseignée avant ce traitement : le blocage redouté ne peut pas se produire en pratique.
Détail —
Il était affirmé que certaines écritures enregistraient les montants dans un format informatique imprécis, avec un risque d'écarts d'arrondi de quelques centimes. La contre-vérification a conclu que les montants concernés ne sont pas exposés à ce risque en pratique et qu'aucun écart n'a pu être produit. Constat infirmé.
Détail —
Le premier passage craignait qu'un bail repris d'un autre gestionnaire redemande au locataire un dépôt de garantie qu'il avait déjà versé. La contre-vérification a montré que le scénario est bloqué deux fois : les contrats repris démarrent directement au cycle de loyer courant, qui ne génère jamais d'appel de dépôt, et la comptabilité rejette de toute façon un tel appel s'il se présentait. Aucun locataire ne peut être facturé deux fois.
Détail — Le scénario est bloqué à deux reprises. (1) adb-contracts/ContractServiceImpl.java:1099 calculateNextCallForRentPeriod (appelée depuis setCalculatedFields:1010, invoquée dans create():145 et updateOne():202) pré-remplit nextCallForRentPeriod pour tout contrat en reprise de gestion, si bien que CallForRentPeriodInfo.java:44 résout RECURRING_RENT, dont friends() (RecurringRent.java:85-133) n'engendre jamais CallForDeposit. (2) Défense en profondeur : adb-accounting PreValidationHandler.java:55-58 lève explicitement EVENT_IS_NOT_SUPPORTED_CONTRACTS_ORIGIN lorsqu'un événement INITIAL_RENT cible rc.isTakeOverContract() ; ce handler s'exécute en troisième dans accountingEventPreHandlerChain (HandlerChainConfig.java:80), avant les handlers de règle de comptabilisation/d'écriture, de sorte que l'événement échoue et qu'aucune écriture 411100→165100 n'est passée. La reprise de gestion n'a pas été oubliée — elle est explicitement rejetée.
Il était affirmé qu'un mécanisme de validation des écritures était mort et que les écritures n'avaient donc aucun cycle brouillon/validé. En réalité, le mécanisme pointé est bien inutilisé, mais le contrôle existe à un niveau supérieur : les simulations ne sont jamais enregistrées, seules les écritures définitivement validées sont écrites en comptabilité, et elles sont ensuite immuables. La garantie recherchée est donc bien en place.
Détail — EntryState est effectivement inutilisé, mais l'écart de cycle de vie allégué existe ailleurs. Le statut brouillon/validé est appliqué en amont : AccountingEventState (adb/adb-model/.../AccountingEventState.java:3) ainsi que la machine à états de la facture (adb-accounting/.../statemachine/InvoiceStateMachine.java, PARKED/BOOKED). Les exécutions provisoires passent par ProposeRepositoryStrategy.saveFullEntries:158, qui ne persiste jamais les écritures ; seul ProcessRepositoryStrategy:186 écrit. Ainsi toute écriture persistée est comptabilisée par construction, et son creationDateTime immuable (AbstractReferencedObject.java:49-54) peut alimenter le ValidDate du FEC. Les champs d'écriture sont final ; DomainObjectEntryRepository.java:12-32 n'expose que add plus les mises à jour de lettrage — une mutation de lettrage que le FEC autorise après validation. Les corrections passent par reverse()/reversedOrigin (AccountingEntry.java:227). Le problème résiduel est une question d'hygiène de code mort : faible.
Le premier passage affirmait que les IBAN n'étaient jamais vérifiés et que des coordonnées bancaires erronées pouvaient se retrouver dans les fichiers de virement SEPA. La contre-vérification a montré qu'un contrôle conforme à la norme internationale des IBAN est bien appliqué côté serveur, et que l'écran de saisie bloque l'enregistrement tant que l'IBAN n'est pas valide. Le risque annoncé n'existe pas.
Détail — Une validation IBAN mod-97 existe : adb/adb-model .../validator/IBANConstraintValidator.java:16-21 utilise IBANValidator d'Apache commons-validator (ISO 13616 mod-97, testé avec des fichiers de fixtures), exposé via @IBAN et adb-utilities ValidationsController.java:30. L'UI de production bloque l'enregistrement du formulaire bancaire tant que cette vérification serveur n'a pas réussi (adb-ui .../bank-form.component.ts:130,299-303 via iban.validator.service.ts), et SepaMandateContractRecap.java:56 annote iban avec @IBAN. La preuve avancée par le constat (« grep pour mod97/validateIban : aucun résultat ») est manifestement fausse. L'écart résiduel réel — BankContract.java:17 non contraint au niveau API/persistance et BIC seulement « requis » — relève de la défense en profondeur, de faible gravité.