Correspondence (mail-intake) module
Parked — not currently shipped
The Correspondence staff UI (apps/web/src/app/dashboard/correspondence/) has been removed, and its API ops routes + worker handlers are parked under archive/ (they were already quarantined out of the build). This note is retained as the reference for reviving the module onto the greenfield tenant spine; the source paths below now live under archive/.
The Correspondence product is a deep integration of the standalone aima-mvp app (Next.js 16 + Supabase) into the monorepo. The Next.js/Supabase/Vercel-cron shell was dropped; the business logic was preserved and re-homed onto the monorepo stack (Fastify + pg-boss + Prisma/Postgres + MinIO/S3 + @breezycorp/* shared packages).
Pipeline
Gmail INBOX (has:attachment)
└─ worker: correspondence.gmail-poll (cron)
sync → GmailIntakeQueue → claim one → download attachment
→ IntakeService.createDocumentFromAttachment (store PDF in MinIO, create CorrespondenceDocument)
→ archive Gmail message → enqueue correspondence.run-ocr
└─ worker: correspondence.run-ocr
OcrClientsService.runForDocument:
Vision/pdf-parse OCR → Claude client-row extraction → per-page PDF split
→ Odoo D3/D4 client + contact match → persist ocrClientsItems (JSON)
└─ worker: correspondence.dispatch-poll (cron)
find completed docs past grace window → honour dispatch_mode
(item_ready | document_complete) → DispatchService sends one Gmail email
per clean item (split PDF attached) → stamp dispatched_atWhere things live
packages/correspondence(@breezycorp/correspondence)logic/— pure: classification rules (+ Claude fallback), entity regex, LLM entity extraction, Odoo match helpers, mail id/subject parsing, ocr-clients review rules.ocr/— Vision + pdf-parse OCR orchestration, barcode/AI PDF splitting, Claude client-row extraction, the purerunOcrClientsPipelineOnPdfBuffer.integrations/— Gmail client + intake helpers, Odoo JSON-RPC + matching, dispatch email builders.ports/—StoragePort+S3StoragePort(MinIO; keys undercorrespondence/<drid>/…in the sharedS3_BUCKET).services/— Prisma-backedIntakeService,OcrClientsService,GmailQueueService,DispatchService,SettingsService.
apps/worker/src/handlers/correspondence/—gmail-poll,run-ocr-clients,dispatch-poll; schedules registered inapps/worker/src/schedules/index.ts(env cronsCORRESPONDENCE_GMAIL_POLL_CRON/CORRESPONDENCE_DISPATCH_POLL_CRON).apps/api/src/routes/ops/correspondence/— staff list/detail/run-ocr/ source-url/settings, gated byREVIEW_CORRESPONDENCE/MANAGE_CORRESPONDENCE_CONFIG.apps/web/src/app/dashboard/correspondence/— staff dashboard (list + run-OCR). A full review screen (PDF viewer + per-item manual match/defer/dispatch) and a settings UI are the next UI increment.packages/db— modelsCorrespondenceDocument,CorrespondenceEntity,GmailIntakeQueue,CorrespondenceSetting; migration…_add_correspondence_module.
Reused vs. kept-raw
- Text Claude calls go through
@breezycorp/ai-claude; image/vision Claude calls (PDF-boundary detection) keep the raw@anthropic-ai/sdk(the shared client is text-only). - Vision OCR keeps aima's GCS async-batch path (
@google-cloud/vision+@google-cloud/storage) — it doesn't fit the synchronous@breezycorp/documentsVisionOcrAdapter, so it was not forced onto it. - Dispatch keeps the Gmail API (thread/reply fidelity) rather than
@breezycorp/notificationsSMTP.
Config
Reuses S3_* (MinIO), the active AI gateway's key (OPENROUTER_API_KEY by default; ANTHROPIC_API_KEY when AI_GATEWAY=anthropic), and GOOGLE_VISION_CREDENTIALS_JSON. Correspondence-only env (see .env.example): GMAIL_*, ODOO_* (matching is optional — ODOO_MATCH_ENABLED), DISPATCH_TO_EMAIL, DISPATCH_FROM_NAME, DISPATCH_GRACE_SECONDS, ENTITY_EXTRACTION_USE_LLM, and the two cron vars.
Dev verification (no cloud creds)
With OCR_PROVIDER=mock / CLAUDE_PROVIDER=mock: pnpm db:seed creates two demo documents (one REVIEW/completed with two extracted items, one fresh INTAKE). The dashboard at /dashboard/correspondence lists them; "Run OCR" enqueues correspondence.run-ocr. Flip dispatch_mode via PATCH /ops/correspondence/settings; the dispatch poll sends to Mailpit (:8025) when DISPATCH_TO_EMAIL is set.
Notable port decisions
- The flattened
document_entitiestable was ported asCorrespondenceEntity; the normalizeduniversal/legal/invoicevariant was deferred (matches aima'sENTITY_EXTRACTION_USE_LLM=falsedefault). odoo-match-helpers: a stale aima test assertedambiguousfor a lone mid-band candidate; the shipped code returnsmatched. The port preserves the live behaviour and aligns the test.