Facturation des frais de gestion porteur¶
Extrait du domain map API, qui était chargé dans chaque session agent. La rule garde l'index module → contexte ; le détail vit ici, à côté du code.
Facturation porteur, zero stockage : la table de correspondance (cf. glossaire) donne le axonautId, tout le reste vit chez Axonaut. Lecture (espace-fi BRI-1656, BO Projets) : 1 seule requete company-filtered (page en HEADER, quota journalier serre — jamais de boucle ; page pleine = les plus anciennes ne sont plus listees → alerte Slack, pas un simple warn) ; 404 Resource not found = societe sans facture → liste vide, 404 Company not found. reste une erreur (sinon une cle pointant sur le mauvais espace Axonaut lirait « aucune facture » partout) ; matching local sur le marker ({projectId}) cherche dans details et dans le libelle de la ligne, balises HTML retirees (Bubble laisse details vide sur certaines familles et y met du rich text sur d'autres, ou un <br/> coupe l'uuid en deux) ; famille de frais resolue par product_code (factures emises par l'API) ou a defaut par libelle catalogue (factures Bubble, sans code) ; avoirs exclus du rapprochement echeance par total negatif mais conserves dans la liste BO (une liste comptable qui les masque se lit faux). Emission (BRI-1658, derriere le flag — cf. glossaire) : POST direct de la facture de frais de fiducie au transfert (produit OBL-FR-006, prix en euros la ou tout le reste est en cents, tax_rate = TVA du projet, description stampee ({projectId}) pour le matching), public_path persistee dans properties.companyFees ; une creation en echec ne bloque pas le transfert — il est enregistre sans URL de facture + alerte Slack. Emission collecte (BRI-1757, meme flag) : POST direct de la facture de frais de collecte chez Invoice Ninja au transfert (product_key fige a l'identique des factures Bubble, prix en euros, sans TVA — emetteur Bricks Origination LLC-FZ a Dubai, hors champ TVA : Bubble droppait deja le vatPercentage et aucune facture Invoice Ninja n'en porte), URL publique = invitations[0].link du client portal ; meme non-bloquant que la fiducie. Provisioning (BRI-1769) : societe Axonaut et client Invoice Ninja (BRI-1770) crees ensemble depuis le SPV via une lecture leniente (getSpvBillingInfoByProjectId — les SPV legacy cassent le validateur complet, un champ billing absent est une erreur metier pas un 500 ; et lue apres l'early-return de la row complete, pour qu'un json pourri ne casse pas un projet deja provisionne) ; le contact est un POST /employees separe dont l'echec alerte Slack sans annuler la creation, non rollbackable. Declencheur (BRI-1657) : le publish enfile un job Graphile dans sa transaction (queue dediee, jobKey par projet) — la publication n'attend jamais Axonaut, le job n'existe que si elle commit, et il throw pour partir en retries + alerte Slack. Dedup par SIREN cote Axonaut, pas cote table : GET /companies?siret= est le seul filtre honore et 96 % des porteurs y sont deja (crees par Bubble), donc on reutilise le premier resultat au lieu de dupliquer, is_disabled → refus ; SIREN et non spvId car un porteur a jusqu'a 6 lignes SPV chez nous pour une societe Axonaut. Dedup cote Invoice Ninja par nom exact : Bubble a laisse id_number vide, donc ?filter= (flou) + egalite stricte sur le nom, clients supprimes exclus ; nos creations posent id_number = SIREN, et un id_number non-vide doit matcher le SIREN (les noms legaux ne sont pas uniques — un homonyme post-cutover ne doit pas capter les factures du porteur). Ordre : les deux flows providers partent en parallele, mais la row nait avec la societe Axonaut, le client Invoice Ninja est patche ensuite — un echec Invoice Ninja laisse la row avec invoiceNinjaId NULL et le retry ne refait que le client ; un echec Axonaut n'ecrit rien, le client Invoice Ninja cree en parallele reste orphelin et le retry le reutilise par nom. Chaque provider etant garde par son propre id, une row backfillee avec un client mais sans societe ne cree que la societe manquante, sans rappeler Invoice Ninja. Surface BO : GET seuls (by-project/:projectId pour la societe, /:companyId/axonaut-invoices, /:companyId/invoice-ninja-invoices sous administration/project-owner-company) — la modale lit, elle ne provisionne pas ; l'onglet Invoice Ninja = client (id + deep-link) + toutes les factures du client, sans filtrage projet : les notes Bubble portent des noms de deal libres qui n'existent que dans Bubble (« Projet : _ Malroy » vs DB « Division fonciere Malroy »), irreconstructibles depuis notre Postgres — filtrer cachait les anomalies ; la note de ligne est renvoyee (lineNote) et affichee en BO pour lever l'ambiguite des clients multi-projets ; le marker ({projectId}) reste stampe a l'emission, un re-filtrage futur reste possible. Contrat commun aux deux providers (projectOwnerInvoice : invoiceId string des deux cotes, productLabel, lineNote) — le BO rend une seule liste, pas de shape par provider. Selection (ordre + eligibilite) en business/resolve-*-invoices, les views/ ne font que projeter