Mortgage Document Upload and AI Analysis
Summary
Acheteurs (or their Pro agents) upload supporting documents to a mortgage application via multipart POST. Accepted formats are PDF, JPEG, PNG, and WebP up to the configured UPLOAD_MAX_SIZE_MB limit. Each file is saved to uploads/mortgage/{applicationId}/{nanoid}.{ext} on disk, a MortgageDocument record is created in pending analysis status, and then Claude Vision analysis runs asynchronously (fire-and-forget via setImmediate). The analysis transitions the document to processing, then either valid, invalid, or error based on Claude’s confidence score and detected anomalies.
State Diagram
stateDiagram-v2 state "Dépôt en cours" as Uploading state "Fichier rejeté" as Rejected state "En attente d'analyse" as Pending state "Analyse en cours" as Processing state "Document valide" as Valid state "Document invalide" as Invalid state "Erreur d'analyse" as Error [*] --> Uploading Uploading --> Rejected: invalid MIME or oversized Uploading --> Pending: file saved, DB record created (201) Pending --> Processing: setImmediate fires analyzeDocument() Processing --> Valid: confidence ≥ 0.5 AND matchesDeclared AND no errors Processing --> Invalid: confidence < 0.5 OR !matchesDeclared OR errors present Processing --> Error: analyzeDocument() throws Valid --> [*] Invalid --> [*] Error --> [*]
Steps
1. Authentication (Actor: system)
The dualAuth preHandler verifies either Acheteur or Pro JWT. Ownership of the application is confirmed via prisma.mortgageApplication.findFirst({ where: ownerFilter }) — returns 404 if the caller does not own the application.
Triggers: Request to POST document Outcome: Application ownership confirmed
2. Multipart Parse (Actor: system)
request.parts() async iterator reads the multipart stream:
filepart: buffered to memory, MIME type and filename captured.categoryfield: required DocCategory enum value.subcategoryfield: required free-text label.
Validations:
- At least one file part must be present → 400
MISSING_FILE - MIME must be one of
application/pdf,image/jpeg,image/png,image/webp→ 400INVALID_MIME_TYPE - File size must not exceed
UPLOAD_MAX_SIZE_MB→ 413FILE_TOO_LARGE - Both
categoryandsubcategorymust be provided → 400MISSING_FIELD
Triggers: File part received in multipart stream
Outcome: fileBuffer, mimeType, category, subcategory populated
3. Persist File to Disk (Actor: system)
Directory uploads/mortgage/{applicationId}/ is created recursively if needed. File is written as {nanoid(12)}.{ext} where ext is derived from MIME type mapping. Relative path mortgage/{applicationId}/{filename} is stored in the DB record (not the absolute path).
Triggers: Successful validation
Outcome: File written to disk, relativeFilePath computed
4. Create DB Record (Actor: system)
prisma.mortgageDocument.create() with analysisStatus: "pending". The 201 response is returned immediately after this step — analysis is non-blocking.
Triggers: File persisted
Outcome: MortgageDocument row created, 201 returned to client
5. Async Claude Vision Analysis (Actor: system, tools: claude-api)
setImmediate fires after the response is sent:
- Update
analysisStatus→"processing". - Call
analyzeDocument(relativeFilePath, mimeType, category, subcategory)fromlib/document-analyzer.ts. - Evaluate result:
finalStatus = "invalid"if:analysisResult.errors.length > 0OR!analysisResult.matchesDeclaredORanalysisResult.confidence < 0.5.- Otherwise
finalStatus = "valid".
- Update
MortgageDocumentwithanalysisStatus,analysisResult,extractedData,analyzedAt.
If analyzeDocument throws: set analysisStatus → "error" with a synthetic analysisResult object containing the error message.
Triggers: HTTP 201 response has been sent
Outcome: Document marked valid, invalid, or error
6. List Documents (Actor: acheteur or pro)
GET /mortgage/applications/:id/documents returns all documents for the application ordered by uploadedAt desc. Ownership verified.
7. Delete Document (Actor: acheteur or pro)
DELETE /mortgage/applications/:id/documents/:docId:
- Verify application ownership.
- Verify document belongs to this application.
- Delete file from disk (
ENOENTignored). - Delete DB record.
- Return 204.
Triggers: User removes a document Outcome: File and DB record deleted
Error States
- No file in request → 400
MISSING_FILE - Unsupported MIME type → 400
INVALID_MIME_TYPE - File exceeds size limit → 413
FILE_TOO_LARGE - Missing
categoryorsubcategory→ 400MISSING_FIELD - Application not found or not owned → 404
NOT_FOUND - Claude API failure → document set to
analysisStatus: "error", upload still succeeds
Related Processes
- application-creation — application must exist before documents can be uploaded
- wizard-steps — at least one document required before submit; document count contributes 25% to completionRate
- broker-assignment-review — broker sees document analysis status in the export