Aller au contenu

Project Financing Request Module

Backend for the Project Financing Request portal — the project-owner-facing interface where borrowers submit their financing requests.

History: Renamed from espace-fi in May 2026 (8-part migration). HTTP routes, S3 provider, config keys, and TypeScript code all use project-financing-request. Most DB tables use the project_financing_request_* prefix; the borrower company table is project_owner_company (renamed from espace_fi_spv, BRI-1501). The shared API contract package remains @bricks-common/api-communication-project-owner.

Stack

Uses the Kysely + Zod stack (see .cursor/rules/api-conventions.mdc § "Stack Espace Financement"):

Tool Purpose
Kysely Query builder (getKysely(), ky_safePgTransaction)
Zod Validation + type derivation
apiErr / throwApiError Structured business errors → HTTP mapping

Database tables

Table Content
project_financing_request Financing request (draftin-analysis → …) linked to a project owner
project_financing_request_owner Owner identity, linked to Better Auth user ID
project_financing_request_presentation JSONB document with pitch fields + S3 image keys
project_owner_company Borrower company data (project owner company)
project_financing_request_offer Versioned financing offer (append-only, pushed by Projet Analyse)

Schemas live in src/__new/lib/kysely/schemas/ (project-financing-request*.schema.ts, project-owner-company.schema.ts).

HTTP endpoints

All routes are prefixed project-financing-request/ and guarded by ProjectOwnerGuard (ensures the authenticated Better Auth user owns the project).

The guard enforces project ownership only when projectId is present in the URL. Routes without projectId (e.g. Pappers search) only require a valid Better Auth session and a project_financing_request_owner row.

Project presentation

Method Path Action
GET /project/:projectId/presentation Get or create presentation (returns presigned GET URLs for images)
PATCH /project/:projectId/presentation/:presentationId Save presentation body (sets status back to draft)
PUT /project/:projectId/presentation/:presentationId/complete Mark as completed

Presentation images (S3 presigned upload)

Method Path Action
POST /project/:projectId/presentation/:presentationId/images/request-upload Get presigned PUT URLs for upload
POST /project/:projectId/presentation/:presentationId/images/confirm-upload Confirm objects exist + persist keys in JSONB
DELETE /project/:projectId/presentation/:presentationId/images Remove image(s) from presentation

Project owner company (emprunteur)

Method Path Action
GET /project/:projectId/project-owner-company Get or create company record
PATCH /project/:projectId/project-owner-company/:projectOwnerCompanyId Update company fields
PUT /project/:projectId/project-owner-company/:projectOwnerCompanyId/complete Mark company as completed

Submit to analysis

Method Path Action
POST /:projectId/submit Submit draft project to analysis (requires presentation + documents completed; company optional)

MkDocs : phase-one-steps.md.

Financing requests

Method Path Action
GET /projects List financing requests (id, name, status, steps, createdAt, updatedAt)
POST /projects Create an empty draft financing request for the authenticated owner

Phase-1 steps (presentation, documents, borrower) are computed per entity. Phase-2 keys (analysis, offer, finalization) are also returned: analysis is derived from macro status; offer / finalization stay unavailable until their step statuses are wired to dedicated entities (offer table + repo exist; owner HTTP endpoints WIP). The PDP app reads steps from the API as-is, maps finalization → UI label collect, and injects a front-only signature step — see phase-one-steps.md.

Project documents

Method Path Action
GET /project/:projectId/documents List files (presigned GET URLs) + onboarding/analysis checklists
POST /project/:projectId/documents/request-upload Presigned PUT URLs for file upload
POST /project/:projectId/documents/confirm-upload Confirm S3 objects + persist document rows
POST /project/:projectId/documents/:documentId/comment Add comment on a file
DELETE /project/:projectId/documents/:documentId/comment Remove comment
PUT /project/:projectId/documents/base-document-list/check Toggle onboarding checklist item (draft only)
PUT /project/:projectId/documents/analysis-documents/check Toggle analysis missing-doc item (blocked only)
PUT /project/:projectId/documents/complete Mark documents step completed (draft only)
POST /project/:projectId/documents/validate Clear analysis missing docs + notify Projet Analyse (blocked)

MkDocs : project-documents.md.

Project analysis (owner)

Method Path Action
GET /project/:projectId/analysis/status Analysis UI state (pending or blocked with questions/requests)
POST /project/:projectId/analysis/question/:questionId/answer Answer a Projet Analyse question
POST /project/:projectId/analysis/reanalysis Request re-analysis from blocked (optional comment)

MkDocs : project-analysis.md.

Internal — Projet Analyse (x-api-key: projet-analyse)

Method Path Action
GET /internal/projet-analyse/project/:projectId Full dossier state: instruction (status, questions, requests) + dossier (presentation, borrower, uploaded documents with presigned URLs) + finalization block once the owner validates the finalization step
POST /internal/projet-analyse/project/:projectId/questions Push a question → blocked
POST /internal/projet-analyse/project/:projectId/comment Push analyst comment → blocked
POST /internal/projet-analyse/project/:projectId/document-requests Push a document request → blocked
PUT /internal/projet-analyse/project/:projectId/missing-documents Sync analysis missing document names
PUT /internal/projet-analyse/project/:projectId/document/:documentId/refuse Mark uploaded file as refused (isRefused on owner API)

Outbound — ping Projet Analyse (M2M, static key)

POST {PROJET_ANALYSE_API_URL}/projects/:projectId/updated with { "reason": … }.

Same shared-secret scheme as the inbound routes above, carried in a different header: Authorization: Bearer <PROJET_ANALYSE_API_KEY> — a static key, not an OAuth token. Separate secret from the inbound x-api-key.

reason Emitted from Transition
submitted ProjectSubmissionService.submit draftin-analysis
reanalysis-requested ProjectFinancingRequestAnalysisWriteService.requestReanalysis blockedin-analysis
missing-documents-provided ProjectFinancingRequestDocumentService.validateStep stays blocked
finalized not emitted yet — arrives with the finalization step finalization

The reason is a hint, never state: Projet Analyse always re-reads the truth from the inbound GET above. A replayed ping carries the same immutable reason, so retries stay idempotent.

Pappers

Method Path Action
GET /pappers/search Company search proxy (SIREN/SIRET lookup)
GET /pappers/company Company details proxy by SIREN (siege + representants)

Onboarding document checklist

Method Path Action
PUT /project/:projectId/documents/base-document-list/check Toggle a checklist item (documentKey, checked) — see Documents

Body schema : toggleProjectFinancingRequestOnboardingBaseDocumentCheckBodySchema (@bricks-common/api-communication-project-owner).

Semantics of documentsStep.expectedDocuments (JSON on the draft project) :

  • true = document still pending (not checked off by owner)
  • false = checked off by owner

The toggle inverts the API checked flag when persisting: documentsStep.expectedDocuments[key] = !checked.

Document keys (projectFinancingRequestOnboardingDocumentKeySchema) — 9 items, all default to true on project creation:

presentationDocument, unilateralPurchasePromise, equityProof, subdivisionPermit, buildingPermit, operationForecast, companyKbis, worksQuote, expertValuationReport

steps.documents in GET /projects (compute-project-financing-request-steps.ts) :

  • Projet draftdocumentsStep.status persisté (unavailable jusqu'à complétion de la présentation, puis draft / completed)
  • Projet soumis (≠ draft) → toujours completed (soumission exige l'étape complétée)

La checklist expectedDocuments n'influence pas ce statut — elle guide l'UX uniquement.

Error HTTP Cause
project-not-draft 409 Project no longer in draft
onboarding-expected-document-not-found 404 Unknown documentKey

S3 presigned upload flow

The image pipeline uses presigned URLs so the browser uploads directly to S3 without proxying through the API.

Client                          API                              S3
  │                              │                               │
  ├── POST /images/request-upload ──►                            │
  │   (file list: type, size)    │                               │
  │                              ├── generateKey (uuid7 + ext)   │
  │                              ├── computePresignedPutUrl ─────►
  │                              │                               │
  │◄─── { uploads: [{ presignedUrl, imageKey, expiresAt }] } ───┤
  │                              │                               │
  ├── PUT presignedUrl (binary) ─────────────────────────────────►
  │                              │                               │
  ├── POST /images/confirm-upload ──►                            │
  │   (imageKeys + positions)    │                               │
  │                              ├── HeadObject (validate exists)─►
  │                              ├── Transaction: update JSONB   │
  │                              ├── Delete replaced S3 keys ────►
  │                              │                               │
  │◄─── presentation with presigned GET URLs ────────────────────┤

Key details

  • S3 key format: projects/${projectId}/presentation-images/${uuidv7}.${ext}
  • Allowed MIME types: Mapped via PROJECT_PRESENTATION_IMAGE_MIME_TO_EXT from @bricks-common/api-communication-project-owner
  • Browser compatibility: The S3 client disables CRC32 checksum (requestChecksumCalculation: 'WHEN_REQUIRED') because browsers cannot send x-amz-checksum-crc32 on presigned PUTs
  • Atomicity: confirm-upload uses ky_safePgTransaction with row locking. Any failing HeadObject aborts the entire confirmation
  • Reads: get-project-presentation.view.ts signs a fresh GET URL per stored key and returns { key, url } pairs. If any signing fails, the entire response fails (prevents index desync between client and server)
  • Image positions: Presentation supports mainImageKey, secondaryImageKeys[] (indexed), and lots[].imageKey (indexed by lot)
  • Declarative save: A save sends the gallery as it should be (absent = keep, null = clear). Dropped keys are re-tagged pending (a lifecycle rule expires them), kept keys are untagged. Keys are never actively deleted — a concurrent save could re-reference a dropped key

Environment

Variable (via Doppler) Purpose
PROJECT_FINANCING_REQUEST_AWS_ACCESS_KEY_ID AWS credentials for the dedicated S3 bucket
PROJECT_FINANCING_REQUEST_AWS_SECRET_ACCESS_KEY AWS credentials
PROJECT_FINANCING_REQUEST_S3_BUCKET_NAME Target bucket
PROJECT_FINANCING_REQUEST_S3_PUT_TTL_SEC Presigned PUT URL lifetime
PROJECT_FINANCING_REQUEST_S3_GET_TTL_SEC Presigned GET URL lifetime

Additional Espace Financement portal config (broader API integration):

Variable Purpose
ESPACE_FINANCEMENT_API_KEY External API auth key
ESPACE_FINANCEMENT_API_URL External API base URL
ESPACE_FINANCEMENT_API_KEY_INTERNAL Internal API auth key
ESPACE_FINANCEMENT_ONBOARDING_REDIRECT_URL Redirect after onboarding

S3 provider

Located at src/__new/lib/providers/s3/project-financing-request-s3.ts. Built on the generic createS3Service helper which exposes computePresignedPutUrl, computePresignedGetUrl, checkHeadObject, and deleteObject.

File layout

project-financing-request/
├── controllers/
│   ├── project-financing-request.controller.ts
│   ├── project-submission.controller.ts
│   ├── project-presentation.controller.ts
│   ├── project-presentation-images.controller.ts
│   ├── project-document.controller.ts
│   ├── project-document-internal.controller.ts
│   ├── project-analysis.controller.ts
│   ├── project-analysis-internal.controller.ts
│   ├── pappers-search.controller.ts
│   └── project-owner-company.controller.ts
├── services/
│   ├── project-creation.service.ts
│   ├── project-presentation.service.ts
│   ├── project-presentation-images.service.ts
│   ├── project-document.service.ts
│   ├── project-analysis-read.service.ts
│   ├── project-analysis-write.service.ts
│   ├── project-analysis.service.ts
│   ├── project-list.service.ts
│   ├── project-editability.service.ts
│   ├── project-owner.service.ts
│   └── project-owner-company.service.ts
├── business/
│   ├── build-presentation-draft.ts
│   ├── collect-presentation-image-keys.ts
│   └── compute-project-financing-request-steps.ts
├── views/
│   ├── get-project-presentation.view.ts
│   └── get-project-analysis-status.view.ts
├── repositories/
│   ├── project.repository.ts
│   ├── project-presentation.repository.ts
│   ├── project-document.repository.ts
│   ├── project-question.repository.ts
│   ├── project-document-request.repository.ts
│   ├── project-owner.repository.ts
│   ├── project-offer.repository.ts
│   └── project-owner-company.repository.ts
├── guards/
│   └── project-financing-request-owner.guard.ts
├── docs/
│   └── … (MkDocs — voir docs/.pages)
└── README.md

Shared API contract

Validators and types are in @bricks-common/api-communication-project-owner (a dedicated package, separate from investor api-communication).

Naming reference

The rename migrated code progressively. This table clarifies which names are used where:

Layer Naming
Module folder project-financing-request/
HTTP route prefix project-financing-request/
DB tables project_financing_request_*, project_owner_company
TypeScript types/schemas ProjectFinancingRequest*, ProjectOwnerPgSchema, ProjectOwnerCompanyPgSchema
Request augmentation request.espaceFiProjectOwnerId
API contract package @bricks-common/api-communication-project-owner
S3 config key envConfig.projectFinancingRequest