Aller au contenu

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 porte kind, linkedTo, isRefused, comment).
  • isRefused / comment = la revue de l'outil d'analyse, comme côté porteur. Chip « Refusé » ; kind: null = chip « Non classé ».
  • uploadedBy vient de better_auth_user (FK document.betterAuthUserId) : l'email est toujours là, le nom seulement si le compte l'a rempli.
  • deleteError = le 409 que Supprimer (et Remplacer) recevrait, null quand l'action est permise : le BO désactive le bouton et affiche la raison. Calculé par business/can-admin-delete-document : document-not-deletable dès que le projet a quitté draft (une pièce qui a servi à l'analyse reste) ; document-linked-to-several-projects sur 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é par business/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.deletedAt posé 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 (404 document-not-found sinon), supprimée comme ci-dessus ; le nouveau lien reprend projet, rattachement, kind, source et documentRequestId, la revue repart vierge. Le body ne porte que le fichier.
  • Modifier = sur place : kind et rattachement s'écrivent sur le même lien (ProjectDocumentRepository.setClassification, le writer du webhook IA), la revue et documentRequestId restent. { type: 'project' } efface companyId et representativeId.
  • Le BO gagne sur l'IA : le webhook ai-platform n'é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 409 no-document-to-classify évite d'enfiler un job qui se terminerait en skipped sans rien dire.
  • Un dépôt admin est toujours typé, source = 'back-office' et sur un projet : le document porte createdByType = 'bricks_admin' et l'id better_auth_user de l'admin. Depuis la fiche Société, un seul document et 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 unique project_document_link_company_idx est supprimé (V202610051100) : (projectId, documentId) porte l'unicité. Les pièces BO ne remontent pas côté porteur (le funnel lit les sources pdp-*) : 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é (404 representative-not-found).
  • Upload en un appel, pas de presigned : le BO envoie le fichier et payload dans un multipart (le précédent createFeedbackEndpoint), l'API pose l'objet sur S3 (putObject, clé générée côté API) puis écrit la ligne. Le portail garde request-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 de ky_safePgTransaction ; un crash du process ou un deleteObject en é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 UPDATE sur link et document) — l'ordre du portail. Depuis la fiche Société, Ajouter verrouille les projets choisis (404 project-financing-request-not-found si 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-add et project-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, projectId obligatoire).
  • kindSetBy (BRI-2437).