Receipt backlog processing¶
All receipt processing and cache rebuild commands submit work through RabbitMQ. Publishers discover files and queue requests; consumers run OCR and persistence. There is no direct-processing fallback when the broker is unavailable.
Manual run¶
With RabbitMQ, the database, receipt storage, and a consumer available:
ledger receipts process /data/receipts/raw/scanned/inbox
receipts process is an alias for queue publication. Configure RABBITMQ_URL,
DATABASE_URL, RECEIPT_SOURCE_ROOT, and HOME_BUDGET_OCR_CACHE. Its summary
reports discovered, skipped, exhausted, cache-missing, and published counts.
Completion appears in consumer logs. Per-receipt PostgreSQL locks serialize
work across consumers. Start a worker with ledger receipts consume and a result
collector with ledger receipts collect; the collector commits OCR results and
extracted expenses to PostgreSQL.
The legacy home-budget-process-receipts and home-budget-scan entry points
use the same receipts process arguments and queue dispatch; old synchronous
report options such as --json and --show-review are no longer supported.
Mac development and K3s use separate OCR caches:
| Environment | Cache location |
|---|---|
| Mac | .ocr_cache in the local repository, configured by HOME_BUDGET_OCR_CACHE in .env.dev. |
| K3s | HOME_BUDGET_OCR_CACHE in .env.k3s, currently /data/receipts/derived/ocr-cache on the home-budget-data PVC. |
Load the Mac settings before running local processing or cache rebuilds:
set -a
source .env.dev
set +a
Keep the Mac cache on local disk, even when reading source receipts from the shared SMB folder. OCR creates cache files as needed. Switching the Mac cache location leaves existing PV cache files intact; only K3s continues using them. Already-running Mac processes retain their previous environment until restarted.
For Mac development, native PostgreSQL on port 5432 and native RabbitMQ on port
5672 stay running as Homebrew services. Enable the worker and collector as
background services with make dev-receipts-start, then submit work with
ledger receipts process --verbose. Check them with make dev-receipts-status
and follow their output with make dev-receipts-logs. After changing .env.dev,
run make dev-receipts-restart. Do not load the obsolete
.local-postgres/runtime.env. Enable local log forwarding with make dev-telemetry-start; see the
local development guide.
See local macOS development for setup and lifecycle commands.
make deploy-k3s reads the K3s cache path from .env.k3s and writes it to the
receipt-runtime-config ConfigMap before starting Argo CD sync. The receipt
CronJob, RabbitMQ worker, and manual scanned-receipt Job read that ConfigMap.
Use a directory below /data so the cache stays on the PV. The ConfigMap is
provisioned outside Git, so Argo CD does not overwrite the chosen value.
Changed values request Deployment restarts through Argo CD; future Jobs use the updated setting,
while already-running Jobs finish with their original setting. Existing cache
files are neither moved nor deleted when the path changes.
To check or provision only these runtime settings:
make k3s-config-check
make k3s-config-apply
Provision this ConfigMap before deploying these manifests for the first time.
Pass --refresh-ocr-cache to bypass cached OCR and reprocess every discovered
receipt, including receipts already marked succeeded or review-required.
To reprocess exactly one receipt, use its full path relative to the configured receipt source root:
ledger receipts reprocess '2026-08-14/receipts_20260814_0001.pdf' --verbose
This publishes one persistent, confirmed message to receipts.v1.work. The
existing queue consumer refreshes that receipt's OCR cache and replaces its
canonical extraction and line items, even if it already succeeded. The
date-relative source reference is preserved. The command exits after RabbitMQ
confirms publication; OCR runs asynchronously in the consumer.
A successful publication ends with output like:
{
"source_reference": "2026-08-14/receipts_20260814_0001.pdf",
"source_sha256": "<receipt SHA-256>",
"request_id": "740023b1-a078-4914-b074-81bd7129bb75",
"queue": "receipts.v1.work",
"status": "queued"
}
queued confirms broker acceptance, not completed extraction. Worker logs report
OCR and persistence progress, then succeeded, review_required, or a retry/dead
queue routing result. review_required means the extraction needs review.
The publisher uses RECEIPT_SOURCE_ROOT and RABBITMQ_URL, with overrides
--receipt-root and --rabbitmq-url. It requires source-file access but no
database connection. The consumer's configuration supplies HOME_BUDGET_OCR_CACHE,
the database DSN, and schemas; the publisher cannot override them.
Each invocation creates a new request UUID. If a publication fails or its
confirmation is lost, retry with the printed --request-id UUID to reuse that
request. The worker records completion atomically with the extraction under the
receipt lock, so redelivering a completed request skips OCR. Each new request
has a fresh three-attempt database budget, independent of earlier processing.
In K3s, first deploy this version through Argo CD and wait for all workers to
finish rolling out. The PreSync database setup adds
budget.receipt_reprocess_requests and budget.receipt_cache_requests without
rebuilding existing ingest state. Workers accept normal v1, full reprocess v2,
and cache-only v3 messages. Finish rolling out consumers before sending new
message versions; old consumers send unsupported messages to the dead queue.
Then publish:
kubectl --context brownrook-k3s1 -n home-budget \
exec deployment/receipt-worker -- \
ledger receipts reprocess '2026-08-14/receipts_20260814_0001.pdf' --verbose
Follow the consumer's progress separately:
kubectl --context brownrook-k3s1 -n home-budget \
logs -f deployment/receipt-worker --tail=100
In RabbitMQ, select vhost receipts and open
Queues and Streams → receipts.v1.work. Ready shows waiting requests;
Unacked shows delivered work awaiting completion. A fast receipt may only be
visible in message-rate charts. RabbitMQ does not keep an acknowledged-message
history; use the request UUID in worker logs to follow the final result.
Use receipts retry for failed or interrupted work that should become eligible
for normal publishing again.
| Command | Scope | Result |
|---|---|---|
receipts retry SOURCE_REFERENCE |
One failed or interrupted receipt | Resets its attempt budget for the next publish or process run; refuses completed receipts. |
receipts reprocess SOURCE_REFERENCE |
One receipt, including completed receipts | Queues a refresh; the consumer replaces its OCR and extraction results. |
receipts process --refresh-ocr-cache |
Every receipt under the supplied root | Queues one full reprocess request per receipt; workers replace extraction results. |
Who checks for missing OCR cache files?¶
The receipt-processor CronJob runs ledger receipts publish every
15 minutes (*/15 * * * *). This publisher discovers source files under
RECEIPT_SOURCE_ROOT, reads their PostgreSQL completion status, and checks for
their cache files under HOME_BUDGET_OCR_CACHE.
| Component | Responsibility |
|---|---|
receipt-processor publisher |
Detect eligible inbox receipts, including completed receipts whose OCR cache is missing, and publish requests. |
RabbitMQ (receipts.v1.work) |
Hold the requests until a consumer receives them. |
receipt-worker consumer |
Run OCR, recreate cache files, replace saved extraction results for full reprocess requests, and acknowledge completion. |
If a completed receipt's cache is missing, the publisher queues a full reprocess
request through RabbitMQ. The worker recreates the cache and replaces saved
extraction results and line items. The publisher never runs OCR, and the worker
does not scan the inbox independently for missing caches.
Both succeeded and review_required receipts are eligible. Existing current
or legacy cache files keep completed receipts skipped. The publication summary
reports these requests as cache_missing (included in published).
Deleting the cache beneath an inbox therefore makes its completed receipts eligible for reprocessing on the next 15-minute publisher run. This does not require flushing PostgreSQL. The publisher and consumers must use the same cache directory; an incorrect publisher cache path could trigger unwanted reprocessing.
The scheduled check runs only while the CronJob is enabled (suspend: false).
For an immediate check, run ledger receipts publish with the same source,
database, and cache settings. ledger receipts process uses the same check.
Both commands submit work through RabbitMQ and return after publication;
consumer logs report processing completion.
Rebuild only the OCR cache¶
To rebuild only the cache without changing database extraction results, use the following command. Pause scheduled publishing through Git and Argo CD before deleting caches for this purpose, and resume after the rebuild completes:
ledger ocr-cache rebuild --verbose
In K3s, run:
kubectl --context brownrook-k3s1 -n home-budget \
exec deployment/receipt-worker -- \
ledger ocr-cache rebuild --verbose
This publishes one receipt.cache-rebuild.v3 message per discovered source,
including receipts already completed in PostgreSQL. --verbose prints confirmed
publication progress, for example [1/12] Queued 2026-08-14/receipts_20260814_0001.pdf.
The JSON summary reports published, the batch request_id, and status: queued.
It confirms submission, not cache completion. Watch worker logs for
status=active, status=cache_ready, cache_results_published, and
retry/dead-letter outcomes; the collector reports durable cache_rebuilt
completion after its database commit.
Limit a repair to one path relative to the receipt root, or skip receipts that already have a current or legacy cache:
ledger ocr-cache rebuild \
--source-reference 2026-08-14/receipts_20260814_0001.pdf \
--verbose
ledger ocr-cache rebuild --missing-only --verbose
Each v3 message carries the batch UUID plus its item index and total. Worker
logs include batch_id, progress, and completed=N/TOTAL. The completed
count comes from atomic markers on the shared receipt volume, so concurrent
pods report one monotonic batch total without requiring database access.
The worker uses its own HOME_BUDGET_OCR_CACHE and rebuilds one receipt at a
time. A shared-PV advisory lock serializes cache writes by source hash. After an
atomic cache replacement, the worker writes a request marker containing the
cache checksum. A duplicate delivery reuses that exact cache and republishes
the idempotent result events instead of repeating OCR. Deleting or changing the
cache invalidates the marker, allowing a new repair to recreate it. Collector
persistence records request completion without changing saved extraction or
ingest completion state. --workers is deprecated and does not start local
processes; concurrency comes from consumer replicas. The rebuild --ocr-cache
option is used only by the publisher for --missing-only; consumers always use
their own configured cache directory.
For an interrupted or uncertain batch publication, rerun with the printed
--request-id UUID. The same batch and source produce the same request identity,
so already-completed requests are skipped. A new invocation without that UUID
creates a fresh rebuild. Partial publication failures report how many messages
were confirmed. Workers retry each request at most three times before dead-lettering.
Full-inbox refresh with receipts process --refresh-ocr-cache uses the same batch
publication and retry behavior, but sends v2 requests that replace extraction.
Receipt-scoped product enrichment¶
After import, product enrichment can be limited to all line items belonging to
one receipt. A numeric --receipt selector is the canonical
budget.expenses.id, not budget.receipt_evidence.id. The command requires
DATABASE_URL and BRAVE_SEARCH_API_KEY; OPENAI_API_KEY enables the AI query
expansion fallback.
The base OCR pass supplies row order and layout. Ordinary product enrichment also evaluates distinct spellings from all retained successful passes in the latest run for each source. It associates rows through unique ordered text matches with price checks, rather than equating OCR output line numbers with Ledger item numbers. Missing duplicate purchases and ambiguous alignments are excluded. These associations are textual evidence, not verified physical image coordinates; very different readings that cannot be aligned remain excluded.
All aligned spellings are searched before AI query expansion. Product scores
use the spelling that supplied the search, and close competing products from
different spellings require review. Saved results retain the readings, source
run/pass/page/line IDs, evaluated product candidates, and selected spelling in
budget.product_enrichment_results.ocr_evidence. Accepted spelling changes do
not alter the original OCR evidence. Existing OCR caches can be reused.
For an existing database, apply the additive schema update without resetting it:
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f sql/product_enrichment.sql
Then rerun receipt enrichment. Explicit receipt selection reevaluates accepted results that predate OCR alternative evidence once; newly accepted results continue to be skipped on subsequent runs. Each additional distinct spelling can add search requests, although repeated queries use the Brave cache.
Run a dry run first:
ledger receipts enrich --receipt 1
The dry run performs searches and prints a JSON summary without saving results,
so the Ledger UI continues to show Not enriched. Check that the reported
item_ids belong to the intended expense, then persist accepted matches:
ledger receipts enrich \
--receipt 1 \
--write-db
The summary includes considered, searched, db_hits, ai_queries,
ai_expanded, accepted, review, and unsupported. review is the number
of matches below the automatic confidence threshold or with competing OCR
product interpretations; it does not open an
interactive prompt. Those items remain unenriched for manual review in Ledger.
Kubernetes¶
Review a collaborative product recommendation¶
The receipt-scoped CLI above evaluates aligned OCR alternatives with retailer searches. To also pool receipt images, local Qwen proposals and Brave evidence, use the Kubernetes collaboration job:
make enrich-products COLLABORATE=1 LIMIT=1 RECEIPT='receipt.pdf' DRY_RUN=1
make enrich-products COLLABORATE=1 LIMIT=1 RECEIPT='receipt.pdf'
Replace the filename with your receipt. LIMIT=1 bounds the run to one item;
it does not process every item on that receipt. The first command validates the
Job without creating it; the second saves collaborative evidence for review.
Open the receipt in Ledger, inspect the recommendation, and explicitly accept
it for the selected item. Saved recommendations do not change accepted products.
Duplicate rows need separate acceptance unless the receipt-wide Apply accepted
matches and category rules action can reuse an accepted exact retailer/text cache
match. That action also repairs unresolved categories without provider calls.
An accepted reading correction leads the row; Import history retains the
imported text. Related OCR observations shows different retained readings
without claiming that nearby OCR lines belong to that exact Ledger row. See the
user guide
and collaboration manual for evidence and model
configuration. After a new run or acceptance, use make receipts-graph and open
the receipt's View receipt knowledge graph link to inspect the updated story.
Scheduled discovery¶
k8s/receipt-processor-cronjob.yaml publishes eligible receipts every 15 minutes.
concurrencyPolicy: Forbid prevents overlapping scheduled discovery runs. The
publisher mounts receipt storage read-only and requests 100m CPU and 256Mi memory.
The manual scanned-receipt Job and default container command also publish work.
Deploy deploy/rabbitmq through Argo CD to supply the broker, long-running
receipt-worker Deployment, and its KEDA ScaledObject. The brownrook-root
GitOps application manages the KEDA operator and CRDs. Base manifests require
the same RabbitMQ secret and a running consumer. Each worker handles one receipt
at a time with shared-PV locks. A worker requests 1 CPU and 4Gi memory, with
limits of 4 CPUs and 8Gi. KEDA keeps one worker warm and scales to at most two,
for a maximum reservation of 2 CPUs and 8Gi. See the
RabbitMQ runbook for rollout, monitoring, scaling
validation, and recovery.
Useful commands:
kubectl -n home-budget get cronjob receipt-processor
kubectl -n home-budget create job --from=cronjob/receipt-processor receipt-processor-manual
kubectl -n home-budget logs job/receipt-processor-manual
kubectl -n home-budget get jobs
Ledger visibility¶
Open /ledger/receipt-processing to view the latest status for each receipt
source hash, including attempt count, last attempt time, completion time, and
last error. Failed receipts retry until their attempt budget is exhausted; an
operator can then reset them with ledger receipts retry after fixing the
cause. Queued publishing skips completed receipts only while their OCR cache
file exists; otherwise it queues full reprocessing. receipts process
uses this same publication policy. --refresh-ocr-cache explicitly queues
full reprocessing for every discovered receipt.
Receipt processing status is operational metadata only. Canonical expenses, evidence, duplicate review, and reconciliation remain available through their existing Ledger pages.