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-fiin May 2026 (8-part migration). HTTP routes, S3 provider, config keys, and TypeScript code all useproject-financing-request. Most DB tables use theproject_financing_request_*prefix; the borrower company table isproject_owner_company(renamed fromespace_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 (draft → in-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 |
draft → in-analysis |
reanalysis-requested |
ProjectFinancingRequestAnalysisWriteService.requestReanalysis |
blocked → in-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
draft→documentsStep.statuspersisté (unavailablejusqu'à complétion de la présentation, puisdraft/completed) - Projet soumis (
≠ draft) → toujourscompleted(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_EXTfrom@bricks-common/api-communication-project-owner - Browser compatibility: The S3 client disables CRC32 checksum (
requestChecksumCalculation: 'WHEN_REQUIRED') because browsers cannot sendx-amz-checksum-crc32on presigned PUTs - Atomicity:
confirm-uploadusesky_safePgTransactionwith row locking. Any failingHeadObjectaborts the entire confirmation - Reads:
get-project-presentation.view.tssigns 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), andlots[].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 |