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:

  • file part: buffered to memory, MIME type and filename captured.
  • category field: required DocCategory enum value.
  • subcategory field: 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 → 400 INVALID_MIME_TYPE
  • File size must not exceed UPLOAD_MAX_SIZE_MB → 413 FILE_TOO_LARGE
  • Both category and subcategory must be provided → 400 MISSING_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:

  1. Update analysisStatus"processing".
  2. Call analyzeDocument(relativeFilePath, mimeType, category, subcategory) from lib/document-analyzer.ts.
  3. Evaluate result:
    • finalStatus = "invalid" if: analysisResult.errors.length > 0 OR !analysisResult.matchesDeclared OR analysisResult.confidence < 0.5.
    • Otherwise finalStatus = "valid".
  4. Update MortgageDocument with analysisStatus, 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:

  1. Verify application ownership.
  2. Verify document belongs to this application.
  3. Delete file from disk (ENOENT ignored).
  4. Delete DB record.
  5. 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 category or subcategory → 400 MISSING_FIELD
  • Application not found or not owned → 404 NOT_FOUND
  • Claude API failure → document set to analysisStatus: "error", upload still succeeds