Documents admin (BO Projets) — fiche Projet et fiche Société¶
Deux familles de routes (AdminAuthGuard) servent l'onglet « Documents » des fiches du BO Projets (BRI-2289, BRI-2279) sur les tables document + project_document_link. Une pièce appartient à son projet (projectId) et est rattachée au projet lui-même, à sa société (companyId) ou à un représentant (representativeId) ; projectId est obligatoire, finalisation comprise (#6565) — voir data-reference. Rien n'est partagé entre projets (règles validées sur Slack) : un fichier déposé depuis la fiche Société sur plusieurs projets porte un lien par projet, chacun avec sa propre revue.
- Fiche Projet :
…/administration/project-financing-request/:projectFinancingRequestId/documents— les pièces du projet, toutes sources confondues, finalisation comprise. Jamais celles d'un autre projet, même rattachées à la société. - Fiche Société :
…/administration/project-owner-company/:projectOwnerCompanyId/documents— toutes les pièces rattachées à la société ou à l'un de ses représentants (actif ou retiré), quel que soit le projet. Une ligne par fichier, avec la liste de ses projets.
| Verbe | Route (les deux fiches) | Effet |
|---|---|---|
| GET | /documents |
Liens vivants (archivedAt nul, document.deletedAt nul), du fichier le plus récent au plus ancien. La fiche Projet ajoute company: { companyName? } \| null et representatives[] (actifs) pour le sélecteur de rattachement. |
| POST | /documents |
Multipart, un fichier à la fois : champ file (type et taille vérifiés par multer : 50 Mo, PDF, image ou Office, sinon 415 / 413) + champ payload (JSON, une seule forme, union stricte) : ajout (name + fiche Projet : kind + linkedTo ; fiche Société : kind + projectIds, les projets choisis, un lien par projet rattaché à la société) ou remplacement (name + replacesDocumentId, type et rattachement hérités). → la ligne écrite. |
| PATCH | /documents/:documentId |
Fiche Projet : { kind?, linkedTo? } (au moins un). Fiche Société : { kind }, posé sur tous les liens du fichier. → la ligne écrite. |
| DELETE | /documents/:documentId |
Fiche Projet : archive le lien de ce projet. Fiche Société : refuse un fichier sur plusieurs projets (409 document-linked-to-several-projects). Hors draft, 409 document-not-deletable. La ligne annonce ce 409 dans deleteError (ci-dessous). |
| GET | /documents/:documentId/download-url |
{ url } signée à la demande sur la clé S3 de la pièce. |
| POST | /documents/classification |
Fiche Projet seulement. Enfile le job de classification de la soumission (project-financing-request-document-classification), quel que soit le statut. 409 no-document-to-classify si aucune ligne n'a needsClassification. Réponse vide : les résultats arrivent plus tard par le webhook ai-platform. |
linkedTo en entrée : { type: 'project' } (le projet lui-même) | { type: 'company' } | { type: 'representative', representativeId }.
Sources : project-document-administration.controller.ts, project-owner-company-document-administration.controller.ts, contrat adminProjectDocument.endpoint.ts (api-communication-bricksoffice).
La ligne rendue¶
Même forme dans les deux fiches, pour que le BO partage ses composants :
{ id, name, kind (null tant que non classé), linkedTo, projects: [{ id, name? }], source, mimeType, size, uploadedAt, uploadedBy: { type, firstName?, lastName?, email }, isRefused, comment?, deleteError, needsClassification }
linkedTo= ce à quoi la pièce est rattachée ; un représentant porte son nom, même retiré de la société. Une pièce de finalisation rend{ type: 'company' }.projects= les projets du fichier : le sien sur la fiche Projet, tous ceux où il a été déposé sur la fiche Société (le lien le plus récent portekind,linkedTo,isRefused,comment).isRefused/comment= la revue de l'outil d'analyse, comme côté porteur. Chip « Refusé » ;kind: null= chip « Non classé ».uploadedByvient debetter_auth_user(FKdocument.betterAuthUserId) : l'email est toujours là, le nom seulement si le compte l'a rempli.deleteError= le 409 que Supprimer (et Remplacer) recevrait,nullquand l'action est permise : le BO désactive le bouton et affiche la raison. Calculé parbusiness/can-admin-delete-document:document-not-deletabledès que le projet a quittédraft(une pièce qui a servi à l'analyse reste) ;document-linked-to-several-projectssur la fiche Société pour un fichier sur plusieurs projets (on retire un lien depuis sa fiche Projet).needsClassification= la pièce part au classifieur : pas de type, ou ni société ni représentant. Calculé parbusiness/needs-document-classification, la règle du job : la modale « Demander la classification » liste exactement ce qui est envoyé.
Les règles¶
| Action | Quand |
|---|---|
| Ajouter | toujours ; depuis la fiche Société, sur les projets choisis (un lien par projet) |
| Modifier le type et le rattachement | toujours, pièce de finalisation incluse ; depuis la fiche Société, le type sur tous les liens du fichier |
| Supprimer | le projet de la pièce est en draft. Depuis la fiche Société, jamais un fichier sur plusieurs projets : chaque lien part de sa fiche Projet. Une pièce d'onboarding (pdp-onboarding) retirée d'un projet dont l'étape Documents est validée la repasse en draft : le porteur revalide avant de soumettre, comme après un retrait depuis le portail |
| Remplacer | = Supprimer + Ajouter en une transaction, même règle ; ne rouvre pas l'étape Documents, le nombre de pièces ne bouge pas |
Une pièce qui a servi à l'analyse reste : dès que son projet quitte draft, elle ne se supprime plus. Le rattachement n'a qu'une limite structurelle : une pièce de finalisation reste liée à sa société (finalization_scope_check), tout autre linkedTo répond 409 document-linked-to-not-editable. Pas de statut société dans le calcul.
Choix¶
- Supprimer = la suppression du portail : lien archivé,
document.deletedAtposé s'il ne reste aucun lien vivant, objet S3 conservé (le BO ne retire sur S3 que son propre dépôt refusé, ci-dessous). - Remplacer (
replacesDocumentId) : la pièce remplacée est cherchée dans la même fiche (404document-not-foundsinon), supprimée comme ci-dessus ; le nouveau lien reprend projet, rattachement,kind,sourceetdocumentRequestId, la revue repart vierge. Le body ne porte que le fichier. - Modifier = sur place :
kindet rattachement s'écrivent sur le même lien (ProjectDocumentRepository.setClassification, le writer du webhook IA), la revue etdocumentRequestIdrestent.{ type: 'project' }effacecompanyIdetrepresentativeId. - Le BO gagne sur l'IA : le webhook
ai-platformn'écrit que les champs vides, un type posé par l'ops n'est jamais écrasé. Limite : « rattachée au projet » et « pas encore rattachée » stockent les mêmes nulls. Une pièce rattachée au projet repart donc à chaque classification, et l'IA peut la rattacher à la société ou à un représentant. - Demander la classification = le job de la soumission : même tâche, même
jobKey(un second appel remplace le job en attente ; si le job tourne déjà, graphile en enfile un second), mêmes champs vides. Pas de règle de statut. Renvoyer une pièce déjà analysée ne coûte rien : la plateforme la reconnaît (hash) et rejoue son premier résultat. Le 409no-document-to-classifyévite d'enfiler un job qui se terminerait enskippedsans rien dire. - Un dépôt admin est toujours typé,
source = 'back-office'et sur un projet : ledocumentportecreatedByType = 'bricks_admin'et l'idbetter_auth_userde l'admin. Depuis la fiche Société, un seuldocumentet un lien{ projectId, companyId }par projet choisi (projectIds) : un projet créé après ne voit pas le fichier (ticket « société existante », BRI-2438). L'index uniqueproject_document_link_company_idxest supprimé (V202610051100) :(projectId, documentId)porte l'unicité. Les pièces BO ne remontent pas côté porteur (le funnel lit les sourcespdp-*) : accepté en V1, la page Documents post-financement du PDP n'affiche que les contrats de signature. - Rattachement : « Société » exige une société liée au projet (409
project-owner-company-not-linked), « Représentant » un représentant actif de cette société (404representative-not-found). - Upload en un appel, pas de presigned : le BO envoie le fichier et
payloaddans un multipart (le précédentcreateFeedbackEndpoint), l'API pose l'objet sur S3 (putObject, clé générée côté API) puis écrit la ligne. Le portail garderequest-upload/confirm-upload: un porteur dépose des lots de fichiers, le BO un seul, et trois appels, un préfixe à vérifier et un HEAD n'apportaient rien ici. Les octets transitent par l'API (multer en mémoire, 50 Mo max, quelques fichiers par jour). L'ordre : fiche connue (404 sans upload) → S3 (échec = 500, rien en base) → transaction (écriture refusée ou cassée = l'objet S3 est retiré par le hook de rollback deky_safePgTransaction; un crash du process ou undeleteObjecten échec laisse un orphelin, comme un dépôt presigné abandonné, jamais une ligne vivante). - Préfixes S3 : fiche Projet →
projects/{pfr.projectId}/documents/(celui du portail) ; fiche Société →companies/{companyId}/documents/. Le type stocké vient de l'extension du fichier reçu, celle que multer a filtrée. - Verrous : projets (par id croissant), société, puis les liens (
FOR UPDATEsurlinketdocument) — l'ordre du portail. Depuis la fiche Société, Ajouter verrouille les projets choisis (404project-financing-request-not-foundsi l'un n'est pas à la société), Supprimer et Remplacer ceux du fichier : leur statut décide, une soumission concurrente attend. - Audit
admin_action:project-financing-request.document-addetproject-owner-company.document-add(payload, le JSON tel qu'envoyé ; jamais le fichier),…document-update(body),…document-delete(params),project-financing-request.document-classification-request(params).
Codes d'erreur¶
| Code | HTTP | Quand |
|---|---|---|
project-financing-request-not-found / project-owner-company-not-found |
404 | entité de la fiche inconnue, ou un projectIds qui n'est pas un projet de la société |
document-not-found |
404 | pièce hors fiche, archivée, supprimée, ou replacesDocumentId introuvable |
representative-not-found |
404 | représentant absent ou retiré de la société du projet |
project-owner-company-not-linked |
409 | « Société » sur un projet sans société |
document-not-deletable |
409 | Supprimer ou Remplacer une pièce lue par un projet qui a quitté draft |
document-linked-to-several-projects |
409 | Supprimer ou Remplacer, depuis la fiche Société, un fichier sur plusieurs projets |
document-linked-to-not-editable |
409 | rattacher ailleurs qu'à la société une pièce de finalisation |
no-document-to-classify |
409 | Demander la classification sans pièce needsClassification |
file-required |
400 | multipart sans champ file |
| — | 415 / 413 / 500 | type refusé par multer / fichier > 50 Mo / S3 a refusé l'objet (rien en base) |
Hors périmètre¶
- Les pièces BO côté porteur (page Documents post-financement) : ticket à part, le modèle n'a pas à bouger.
- Un projet créé après un dépôt fiche Société ne reçoit pas le fichier : ticket « société existante » (copie des pièces,
projectIdobligatoire). kindSetBy(BRI-2437).