Aller au contenu

Project owner company (emprunteur)

Contrôleur HTTP de gestion de la société emprunteuse (ProjectOwnerCompany) attachée à une demande de financement côté porteur de projet : informations entreprise et représentants. Statuts : draft (édition libre), completed (identité et représentants validés — l'étape Emprunteur est faite pour chaque dossier qui pointe cette société), finalized (posé par le premier POST …/finalization/complete — fige la banque). La finalisation d'un dossier se lit sur son statut (finalization puis signature), pas sur celui de la société.

La société est facultative pour la soumission : le porteur peut soumettre son dossier en analyse (POST /submit) sans l'avoir complétée. La step s'ouvre alors deux fois — en phase 1 dès que les documents sont completed, puis, s'il l'a sautée, à l'acceptation de l'offre. Partout ailleurs elle est fermée, et l'écriture suit exactement cette ouverture (isBorrowerEditable) : le stepper n'annonce jamais une step que l'API refuserait d'écrire. Si la société est absente ou encore draft à la soumission, isBorrowerStepDeferred est posé à true (immuable) et l'app PDP affiche l'étape emprunteur en phase 2 après l'offre ; la compléter fait alors passer le dossier en finalization. La règle est vérifiée sous lock (projet + société) dans save/complete. Compléter la société ne déclenche plus la soumission (découplée dans POST /submit). Compléter une société différée relance la classification des pièces : à la soumission, le classifieur n'avait ni société ni représentants à proposer.

Source : project-owner-company.controller.ts.

Tous les endpoints requièrent un porteur de projet authentifié via better-auth (ProjectFinancingRequestUserGuard). Comme la route porte un :projectId, le guard vérifie aussi que le projet appartient à ce porteur (sinon 403 Forbidden) et qu'il existe (sinon 404 Not Found).

GET /project-financing-request/:projectId/project-owner-company

Récupère la société du projet (project_financing_request.companyId). Si elle n'existe pas encore, en crée une vide en status='draft' avec company={}, sans représentant, et la rattache au dossier (idempotent : pattern getOrCreate). La création verrouille le dossier (FOR UPDATE) : deux premières lectures simultanées ne créent qu'une société. Voir project-owner-company.controller.ts.

Path params

  • projectId — id de la demande de financement

Response

Renvoie directement le ProjectOwnerCompanyPgSchema.Json (pas de mapper) :

{
  "id": "uuid",
  "status": "draft | completed | finalized",
  "company": {
    "companyName": "string?",
    "siren": "string?",
    "legalForm": "SAS | SASU | SARL | EURL | SCI | SA | SNC ?",
    "shareCapital": "Cents?",
    "headquartersAddress": "string?",
    "postalCode": "string?",
    "city": "string?",
    "country": "string?",
    "rcsCity": "string?"
  },
  "representatives": [
    {
      "id": "string",
      "projectUserId": "string?",
      "firstName": "string?",
      "lastName": "string?",
      "birthDate": "string?",
      "birthPlace": "string?",
      "postalAddress": "{ street, postalCode?, city? }?",
      "residenceCountry": "string?",
      "professionalEmail": "string?"
    }
  ],
  "createdAt": "Date",
  "updatedAt": "Date"
}

Erreurs

Aucune erreur métier nominale (création faite à la volée si absente). Erreurs DB renvoyées telles quelles via throwApiError.

PATCH /project-financing-request/:projectId/project-owner-company/:projectOwnerCompanyId

Auto-save de la société. Le statut est forcé à draft. Note : à la différence du save de présentation, ce handler remplace intégralement company (body.company ?? {}). Les représentants vivent dans project_owner_company_representative : la liste reçue (body.representatives ?? []) est comparée par id aux lignes de la société. Un id inconnu est inséré en individual, un id connu réécrit ses champs App PDP sur sa personne physique (un représentant business garde sa personne morale), un représentant absent de la liste est archivé (archivedAt), jamais supprimé. Les champs saisis au BO (title, pays de naissance, nationalité) survivent au save. Voir project-owner-company.controller.ts.

Path params

  • projectId — id de la demande de financement
  • projectOwnerCompanyId — id de la société à mettre à jour : doit être celle du dossier. Le save ne crée jamais de société, le GET s'en charge

Request body

Validé via saveProjectBorrowerBodySchema :

{
  "company": {
    "companyName": "string?",
    "siren": "string?",
    "legalForm": "SAS | SASU | SARL | EURL | SCI | SA | SNC ?",
    "shareCapital": "Cents?",
    "headquartersAddress": "string?",
    "postalCode": "string?",
    "city": "string?",
    "country": "string?",
    "rcsCity": "string?"
  },
  "representatives": [
    {
      "id": "string",
      "firstName": "string?",
      "lastName": "string?",
      "birthDate": "string?",
      "birthPlace": "string?",
      "postalAddress": "{ street, postalCode?, city? }?",
      "residenceCountry": "string?",
      "professionalEmail": "string?"
    }
  ]
}

Les deux champs racine sont optionnels (omis = remplacé par {} / []).

Response

Le draft persisté (ProjectOwnerCompanyPgSchema.Draft) — même shape que GET mais en draft.

Erreurs

Code Statut Cause
validation-body 400 Body invalide
project-owner-company-not-found 404 Aucune société pour ce projet (le GET ne l'a pas encore créée)
project-owner-company-id-mismatch 404 Le projectOwnerCompanyId du path n'est pas la société du dossier
representative-not-found 404 Un id de représentant appartient à une autre société
borrower-not-editable 409 Société ou projet hors fenêtre d'édition (isBorrowerEditable)

PUT /project-financing-request/:projectId/project-owner-company/:projectOwnerCompanyId/complete

Verrouille la société en status='completed'. Re-valide tous les champs requis via ProjectOwnerCompanyPgSchema.completedSchema (y compris company.rcsCity, utilisé dans les contrats) et chaque représentant actif via ProjectOwnerCompanyRepresentativePgSchema.completedRow — au moins un requis. json étant re-validé à chaque lecture, une ligne completed à laquelle il manque un champ requis devient illisible : d'où le backfill livré avec BRI-1790. Vérifie que le :projectOwnerCompanyId correspond bien à la société trouvée pour le projet (sinon mismatch). Voir project-owner-company.controller.ts.

Path params

  • projectId — id de la demande de financement
  • projectOwnerCompanyId — id de la société à compléter

Response

La société en version completed (branche completed de ProjectOwnerCompanyPgSchema.Json).

Erreurs

Code Statut Cause
project-owner-company-not-found 404 Aucune société pour ce projet
project-owner-company-id-mismatch 404 Le projectOwnerCompanyId du path ne correspond pas à la société stockée pour ce projet
project-owner-company-incomplete 400 Champs requis manquants (le détail Zod est dans body)
representative-not-project-member 400 Un signataire porte un projectUserId sans rôle actif sur le projet
apporteur-affaires-cannot-be-representative 400 Un signataire désigné est apporteur d'affaires sur le projet
borrower-not-editable 409 Société ou projet hors fenêtre d'édition (isBorrowerEditable)

Signataires : promotion et invitations (BRI-1787)

La complétion est le seul chemin vers le rôle representative (décision produit du 05/08). Chaque signataire y est traité selon son état :

Signataire Traitement
projectUserId rempli Le membre est promu representative, dans la transaction
projectUserId absent Une invitation portant representative part vers son professionalEmail, après le commit

Les invitations sortent de la transaction parce que invite() ouvre la sienne et envoie un mail qu'aucun rollback ne rattrape.

grantRepresentative absorbe les deux conflits de invite() au lieu de les propager : une adresse déjà membre voit son rôle promu, une invitation déjà en attente voit le sien élevé pour que l'acceptation accorde le bon — sauf apporteur_affaires, jamais élevé. Les ignorer laisserait le signataire membre sans jamais être signataire.

Un apporteur_affaires n'est jamais promu. Le rôle est mono-valué : l'élever effacerait sa qualité d'apporteur et lui ouvrirait l'offre et la signature, que la matrice de BRI-1788 lui refuse. La règle vaut sur les chemins — désignation par projectUserId (refus de la complétion), désignation par email d'un membre (rôle laissé intact), invitation pending apporteur_affaires (rôle laissé intact), et acceptation d'une invitation representative (rôle laissé intact).

L'adresse d'un signataire doit être délivrable à la complétion (email: z.email() sur l'identité complétée). Le fil, lui, reste en draftEmail : le formulaire s'autosauvegarde 1,5 s après chaque frappe, hors validation, donc le PATCH ne peut pas exiger une adresse finie. La complétion est le premier moment où elle doit l'être.

Un échec d'invitation après le commit est journalisé sans faire échouer la complétion : la société est valide et le signataire est enregistré. L'inverse échouerait un écran dont l'action a réussi, sans possibilité de rejeu (borrower-not-editable). La réparation passe par la page Membres.

PUT …/complete ne déclenche plus la soumission en analyse. Voir Phase 1 — soumission (POST /submit).

Les coordonnées bancaires (banking) ne sont pas saisies sur cet écran : elles sont persistées via PATCH …/finalization une fois l'offre acceptée — voir project-finalization.md.

Liens