Kelvin Perez / Invoice and Receipt Processor

Invoice and Receipt Processor: architecture and explanation guide

This guide explains the implemented controlled demonstration and how to present it. The hosted Trigger.dev control plane schedules a local macOS development worker. The portfolio walkthrough replays retained evidence; a public visitor execution gateway is still pending. Self-hosting Trigger.dev is not required.

Start with the business problem

An operator receives a PDF and needs vendor, amount and sometimes an invoice due date in an accounting record. Reading the PDF is only part of the job. The operator also needs to know whether the amount is interpretable, which fields are missing, and whether the same transaction was already recorded.

The system turns that process into four distinct responsibilities: extract a draft, validate it, ask a person when uncertain, and record the accepted result once. It does not make payments or tax decisions.

The actual architecture

Text equivalent: Trigger.dev starts the local task. The task reads a bounded private PDF, snapshots it, extracts draft values with evidence, and validates them. Uncertain records wait in durable review. A human correction is validated again. Accepted records pass through a write-intent ledger to CSV. An uncertain write is reconciled by reading and comparing its destination row.

The public portfolio receives sanitized screenshots, readbacks and explanatory artifacts. It does not receive private runtime credentials or access to the operator queue.

Follow receipt 002 through the code

  1. src/intake.ts accepts a storage filename, rejects path separators, resolves it within the private storage directory and bounds the byte read. It computes a SHA-256 hash and writes an immutable PDF snapshot. A renamed identical source reuses the existing record. A changed file with the same name gets a version requiring review.
  2. src/pdf.ts checks PDF signature, size, page count and encryption. Digital documents use Poppler text extraction. Scanned documents use image extraction or rendering and local OCR. Subprocesses have time and output bounds. The tested real-layout path uses Apple Vision on macOS. The Tesseract fallback is not established as equivalent for these layouts.
  3. src/receipt.ts reconstructs spatial OCR rows and identifies total/tender evidence. Receipt 002 contains TOTAL SALES 28,000. It proposes 28000.00, preserving the original line and a separator-confirmation warning. It does not read the benchmark annotation.
  4. src/document.ts checks fields and evidence. Receipt 002 has no verified vendor or currency, so it cannot pass. A receipt may omit a due date. An invoice must include both number and a valid calendar due date.
  5. src/processor.ts admits the record with status review, reasons and a revision. The private browser can show the original document beside the draft. The record survives a worker restart because state is persisted on disk.
  6. Receipt 002 stays in review. No accounting export happens. A separate synthetic invoice demonstrates correction and output, so the demonstration never fills a real receipt with invented identity information.

Why a normalized amount is not a final answer

1.234,56 and 1,234.56 both have an interpretable mixed-separator pattern. They normalize to 1234.56. Space, nonbreaking-space and apostrophe grouping are supported with strict group sizes. Inconsistent patterns fail rather than losing all punctuation.

A single comma or period followed by three digits is ambiguous across locales. 28,000 is treated as a grouping proposal requiring review. The sample annotation later establishes that 28000.00 matches this receipt's reference total; the parser itself never consults that answer. Currency is a separate field and is never inferred from the separator or country.

Money validation uses exact decimal strings and BigInt minor units. A positive two-place amount is required. Subtotal and tax must both be absent or sum exactly to the total. The system performs no currency conversion.

What Trigger.dev owns

src/trigger/process.ts defines the invoice-process-document task and a queue concurrency limit of one. Hosted Trigger.dev provides the execution record, attempt history and scheduling. The document processing runs on the development worker, not inside the portfolio browser.

The retry policy explicitly sets four maximum attempts, which means the first attempt plus three retries. Backoff is 1, 2 and 4 seconds, capped at 10 seconds. Development retries are explicitly enabled in trigger.config.ts.

The controlled failure switch throws a synthetic transient error before processing, proving actual retry behavior. Permission, invalid intake, permanent state failures and ambiguous writes abort automatic retries. This demonstrates the chosen failure categories; it is not a claim that every possible operating-system error is classified as transient.

The task returns record identity/status and, when present, a destination receipt. PDF bytes and OCR text are not placed in ordinary task payloads or logs.

What the private store owns

The Processor persists document identity, source reference, draft fields, evidence, validation reasons, status, version, run references, decisions, accounting marker and destination receipt.

Changes use an atomic directory lock. A competing caller gets a busy response. State is saved to a temporary file, synchronized and renamed. The lock's owner must be inspected after a crash; the system does not assume that an old timestamp makes it safe to steal.

This is a single shared-filesystem design. It is appropriate to the demonstrated worker, but cannot be deployed unchanged across ephemeral distributed task containers. A public production implementation needs a durable shared state and object-storage interface, plus deployed extraction capability.

Review is a state transition

The ordinary path is review to accepted to reconcile to exported. Rejection ends at rejected. A valid initial extraction may start at accepted.

reconcile means an accounting effect may have happened. It is deliberately different from an ordinary retryable processing failure.

A review decision includes an actor, timestamp, original revision, before/after snapshots and reason. Approval requires the current version and a reviewable status. Invalid corrections fail validation. Another record with the same business identity prevents approval without reconciliation. Rejected or still-invalid records cannot export.

The private server binds to loopback only, checks Host and Origin, requires a per-process CSRF token and does not allow cross-origin access. It uses the local OS operator as the reviewer. It is not a public multiuser authentication system.

Duplicate protection and the difficult failure

There are two identities. A content hash identifies the exact bytes of a PDF. A business key identifies the intended accounting transaction using normalized vendor, document type, invoice number and currency. Unnumbered receipts use content identity. A new task run ID does not establish a new transaction.

Consider this failure:

  1. The record is accepted.
  2. The processor saves reconcile and a stable marker before appending.
  3. The accounting row is appended and synchronized.
  4. A timeout occurs before success can be recorded locally.

Blindly replaying the append would create a duplicate. Instead, the next execution reads the marker and compares the complete expected CSV row. Exactly one matching row establishes the effect and records its receipt. Missing, multiple or changed rows stop for operator reconciliation. Marker existence alone is insufficient.

This proof is specific to the controlled CSV adapter. A future QuickBooks adapter would need its own correlation and readback contract; QuickBooks is not integrated in this demonstration.

Evidence and measurements

These are development observations, not a held-out accuracy benchmark or measured client impact. No staff-time reduction or financial benefit has been measured.

Retention and operating limits

Private raw PDFs, extracted evidence and review decisions have a seven-day operator-driven expiry path. Minimal completed/rejected duplicate/effect records are pruned after 90 days. Unresolved ambiguous effects retain their markers for reconciliation. Expiry is a CLI operation, not an installed schedule. The observed Free-plan execution-log retention was one day at setup.

The pipeline bounds PDFs to 5 MB and five pages, extracted text to 64,000 characters and task duration to 180 seconds. Limits and timeouts constrain work; they do not establish comprehensive support for arbitrary PDFs.