Add receipt upload and camera capture to the maintenance record form. OCR extracts maintenance-specific fields (service name, date, cost, shop name, odometer) and auto-populates the form. Receipts are stored as documents and linked to the maintenance record.
This mirrors the existing fuel receipt OCR flow (ReceiptOcrReviewModal) but targets maintenance-specific field extraction rather than fuel-specific fields.
Requirements
Receipt Upload on Maintenance Record Form
Add "Scan Receipt" button to MaintenanceRecordForm (mirrors fuel log pattern)
Camera capture on mobile via shared CameraCapture component
File upload fallback for desktop (PDF, PNG, JPG, HEIC)
Pro tier gated (maintenance.receiptScan feature flag)
OCR Extraction for Maintenance Receipts
New extraction type in Python OCR microservice: maintenance receipt
Extracted fields (full extraction):
Service name / description (map to category + subtypes)
Date of service
Total cost
Shop / business name
Parts breakdown (if present)
Labor cost (if present)
Odometer reading (if present)
Vehicle info (if present, for multi-vehicle disambiguation)
Confidence scores per field
Sync processing for images, async for PDFs (mirrors existing pattern)
Accepted OCR results map to MaintenanceRecordForm fields via setValue()
Field mapping: service name -> category + subtypes, date -> date, cost -> cost, shop -> shopName, odometer -> odometerReading
User reviews pre-filled form before submitting
Document Storage and Linking
Store receipt image/PDF as a document via existing documents feature
New migration: Add receipt_document_id (nullable FK) to maintenance_records table
Link created maintenance record to stored document
Display linked receipt on maintenance record detail view (thumbnail + view action)
Cascade: deleting a maintenance record does NOT delete the document
Backend API
New proxy endpoint: POST /api/ocr/extract/maintenance-receipt
New OCR microservice endpoint for maintenance receipt extraction
Update POST /api/maintenance/records to accept optional receiptDocumentId
Update GET /api/maintenance/records/:id to include receipt document metadata
Technical Considerations
Python OCR microservice: New extraction pipeline using pattern matching + Gemini for semantic field extraction (maintenance receipts are more varied than fuel receipts)
Reuse shared CameraCapture, confidence indicator, and review modal patterns from fuel logs
Review modal displays extracted fields with confidence and inline editing
Category and subtypes are suggested from extracted service description
Accepted results auto-populate the maintenance record form
Receipt is stored as a document and linked to the created maintenance record
Linked receipt is viewable from maintenance record detail
Non-Pro users see tier gate (lock icon + upgrade dialog)
Mobile and desktop responsive
## Summary
Add receipt upload and camera capture to the maintenance record form. OCR extracts maintenance-specific fields (service name, date, cost, shop name, odometer) and auto-populates the form. Receipts are stored as documents and linked to the maintenance record.
This mirrors the existing fuel receipt OCR flow (`ReceiptOcrReviewModal`) but targets maintenance-specific field extraction rather than fuel-specific fields.
## Requirements
### Receipt Upload on Maintenance Record Form
- Add "Scan Receipt" button to `MaintenanceRecordForm` (mirrors fuel log pattern)
- Camera capture on mobile via shared `CameraCapture` component
- File upload fallback for desktop (PDF, PNG, JPG, HEIC)
- Pro tier gated (`maintenance.receiptScan` feature flag)
### OCR Extraction for Maintenance Receipts
- New extraction type in Python OCR microservice: maintenance receipt
- **Extracted fields** (full extraction):
- Service name / description (map to category + subtypes)
- Date of service
- Total cost
- Shop / business name
- Parts breakdown (if present)
- Labor cost (if present)
- Odometer reading (if present)
- Vehicle info (if present, for multi-vehicle disambiguation)
- Confidence scores per field
- Sync processing for images, async for PDFs (mirrors existing pattern)
### Review Modal
- `MaintenanceReceiptReviewModal` component (mirrors `ReceiptOcrReviewModal`)
- Display extracted fields with confidence indicators (4-dot system)
- Inline field editing before acceptance
- Category/subtype suggestion from service description
- User can override all fields
- Mobile: bottom sheet drawer / Desktop: dialog modal
### Auto-populate Maintenance Record Form
- Accepted OCR results map to `MaintenanceRecordForm` fields via `setValue()`
- Field mapping: service name -> category + subtypes, date -> date, cost -> cost, shop -> shopName, odometer -> odometerReading
- User reviews pre-filled form before submitting
### Document Storage and Linking
- Store receipt image/PDF as a document via existing documents feature
- New migration: Add `receipt_document_id` (nullable FK) to `maintenance_records` table
- Link created maintenance record to stored document
- Display linked receipt on maintenance record detail view (thumbnail + view action)
- Cascade: deleting a maintenance record does NOT delete the document
### Backend API
- New proxy endpoint: `POST /api/ocr/extract/maintenance-receipt`
- New OCR microservice endpoint for maintenance receipt extraction
- Update `POST /api/maintenance/records` to accept optional `receiptDocumentId`
- Update `GET /api/maintenance/records/:id` to include receipt document metadata
## Technical Considerations
- Python OCR microservice: New extraction pipeline using pattern matching + Gemini for semantic field extraction (maintenance receipts are more varied than fuel receipts)
- Reuse shared `CameraCapture`, confidence indicator, and review modal patterns from fuel logs
- `useMaintenanceReceiptOcr` hook (mirrors `useReceiptOcr`)
- Subscription tier check via `useTierAccess('maintenance.receiptScan')`
- Mobile-first: 44px touch targets, responsive layout, collapsible secondary fields
## Dependencies
- None (existing OCR infrastructure is sufficient)
## Acceptance Criteria
- [ ] "Scan Receipt" button appears on maintenance record form (Pro tier)
- [ ] Camera capture works on mobile; file upload works on desktop
- [ ] Maintenance receipt OCR extracts: service name, date, cost, shop name, odometer
- [ ] Review modal displays extracted fields with confidence and inline editing
- [ ] Category and subtypes are suggested from extracted service description
- [ ] Accepted results auto-populate the maintenance record form
- [ ] Receipt is stored as a document and linked to the created maintenance record
- [ ] Linked receipt is viewable from maintenance record detail
- [ ] Non-Pro users see tier gate (lock icon + upgrade dialog)
- [ ] Mobile and desktop responsive
egullickson
changed title from Email Receipt Ingestion via Resend API to feat: Email Receipt Ingestion via Resend API2026-01-04 05:20:48 +00:00
egullickson
added this to the Sprint 2026-01-05 milestone 2026-01-11 18:44:57 +00:00
egullickson
changed title from feat: Email Receipt Ingestion via Resend API to feat: Maintenance Receipt Upload with OCR Auto-populate2026-02-13 02:45:59 +00:00
Constructor: initialize base ReceiptExtractor and Gemini module
extract(image_bytes, content_type) method:
a. Call base ReceiptExtractor.extract() to get OCR text via preprocessing pipeline
b. Send OCR text to Gemini with structured prompt requesting JSON output:
serviceName (string) - service performed
serviceDate (string, YYYY-MM-DD)
totalCost (number)
shopName (string)
laborCost (number, optional)
partsCost (number, optional)
partsBreakdown (list of {name, cost}, optional)
odometerReading (number, optional)
vehicleInfo (string, optional)
c. Cross-validate with regex: date_matcher for serviceDate, currency_matcher for costs, numeric pattern for odometer
d. Adjust confidence: boost 1.1x if regex confirms Gemini value, reduce 0.8x if mismatch
e. Return ReceiptExtractionResult with per-field ExtractedField(value, confidence)
Acceptance Criteria: Scan button appears (tier-gated). Auto-populate works. Receipt stored and linked. Receipt viewable on detail. Mobile and desktop responsive.
M1 and M2 can potentially run in parallel (OCR microservice is independent of backend). M3 depends on M2 (needs proxy endpoint). M4 depends on M3 (needs hook and modal).
Risk Assessment
Risk
Mitigation
Gemini response quality varies
Cross-validation adjusts confidence; user can edit all fields
Latency concern (3-6s)
Loading spinner with progress indication; acceptable for Pro feature
Category mapping accuracy
Keyword-based with fallback to "Other"; user always reviews suggestion
HEIC format support
Already handled by CameraCapture + OCR preprocessor
Verdict: AWAITING_REVIEW | Next: QR plan-completeness review
## Plan: Maintenance Receipt Upload with OCR Auto-populate
**Phase**: Planning | **Agent**: Planner | **Status**: AWAITING_REVIEW
---
### Decision Critic Result
**Decision**: OCR extraction approach for maintenance receipts.
**Verdict**: REVISE to **Gemini-primary extraction with regex cross-validation**.
- Send OCR text (from existing preprocessing + Tesseract/PaddleOCR) to Gemini text API for ALL field extraction
- Use regex patterns to VALIDATE and adjust confidence on structured fields (dates, amounts, odometer)
- Follows `FuelReceiptExtractor` cross-validation pattern (extract first, validate second)
- Avoids two parallel extraction paths; single Gemini prompt extracts everything
- Matches issue spec: "pattern matching + Gemini for semantic field extraction"
### Sub-Issue Decomposition
| Milestone | Sub-Issue | Domain | Agent |
|-----------|-----------|--------|-------|
| M1 | #150 - OCR microservice extraction pipeline | Python OCR | Platform Agent |
| M2 | #151 - Backend migration and API updates | Backend | Feature Agent |
| M3 | #152 - Frontend OCR hook and review modal | Frontend | Frontend Agent |
| M4 | #153 - Form integration, tier gating, receipt display | Frontend | Frontend Agent |
**Branch**: `issue-16-maintenance-receipt-ocr` (ONE branch for all milestones)
**PR**: ONE PR targeting main, closes #16, #150, #151, #152, #153
---
### Milestone 1: OCR Microservice Extraction Pipeline (#150)
**Goal**: Add maintenance receipt extraction endpoint to Python OCR microservice.
**Files**:
| File | Action | Description |
|------|--------|-------------|
| `ocr/app/extractors/maintenance_receipt_extractor.py` | NEW | MaintenanceReceiptExtractor class |
| `ocr/app/patterns/maintenance_receipt_patterns.py` | NEW | Regex cross-validation patterns for dates, amounts, odometer |
| `ocr/app/routers/extract.py` | MODIFY | Add `POST /extract/maintenance-receipt` route |
| `ocr/app/models/` | MODIFY | Add Pydantic response schema for maintenance receipt extraction |
**Implementation Details**:
1. **MaintenanceReceiptExtractor** (mirrors `FuelReceiptExtractor`):
- Constructor: initialize base `ReceiptExtractor` and Gemini module
- `extract(image_bytes, content_type)` method:
a. Call base `ReceiptExtractor.extract()` to get OCR text via preprocessing pipeline
b. Send OCR text to Gemini with structured prompt requesting JSON output:
- `serviceName` (string) - service performed
- `serviceDate` (string, YYYY-MM-DD)
- `totalCost` (number)
- `shopName` (string)
- `laborCost` (number, optional)
- `partsCost` (number, optional)
- `partsBreakdown` (list of {name, cost}, optional)
- `odometerReading` (number, optional)
- `vehicleInfo` (string, optional)
c. Cross-validate with regex: `date_matcher` for serviceDate, `currency_matcher` for costs, numeric pattern for odometer
d. Adjust confidence: boost 1.1x if regex confirms Gemini value, reduce 0.8x if mismatch
e. Return `ReceiptExtractionResult` with per-field `ExtractedField(value, confidence)`
2. **Cross-validation patterns** (`maintenance_receipt_patterns.py`):
- Odometer regex: `/(\d{1,3}[,.]?\d{3})\s*(mi|miles|km|odometer)/i`
- Labor/parts split validation: parts + labor should approximate total
- Date range validation: not future, not more than 1 year old
3. **FastAPI endpoint**: `POST /extract/maintenance-receipt`
- Input: file (UploadFile), content_type hint (optional)
- Max file size: 10MB
- Response: `{ success, receiptType: "maintenance", extractedFields: { fieldName: { value, confidence } }, rawText, processingTimeMs }`
**Acceptance Criteria**: Endpoint returns structured fields with confidence scores. Gemini extracts semantic fields. Regex cross-validates structured fields.
---
### Milestone 2: Backend Migration and API Updates (#151)
**Goal**: Add receipt document linking to maintenance records and OCR proxy endpoint.
**Files**:
| File | Action | Description |
|------|--------|-------------|
| `backend/src/features/maintenance/data/migrations/XXX_add_receipt_document_id.sql` | NEW | Add receipt_document_id FK |
| `backend/src/features/maintenance/domain/maintenance.types.ts` | MODIFY | Add receiptDocumentId to types and schemas |
| `backend/src/features/maintenance/data/maintenance.repository.ts` | MODIFY | Update mapRow, create/get queries |
| `backend/src/features/maintenance/domain/maintenance.service.ts` | MODIFY | Accept receiptDocumentId on create |
| `backend/src/features/maintenance/api/maintenance.controller.ts` | MODIFY | Pass through receiptDocumentId, return receipt metadata |
| `backend/src/features/maintenance/api/maintenance.routes.ts` | MODIFY | Add proxy route |
| `backend/src/features/ocr/api/ocr.controller.ts` | MODIFY | Add maintenance receipt proxy handler |
| `backend/src/core/config/feature-tiers.ts` | MODIFY | Add maintenance.receiptScan feature key |
**Implementation Details**:
1. **Migration**: `ALTER TABLE maintenance_records ADD COLUMN receipt_document_id UUID REFERENCES documents(id) ON DELETE SET NULL;`
- Nullable FK, ON DELETE SET NULL (deleting document nullifies link, deleting record leaves document)
2. **Types update**:
- Add `receiptDocumentId?: string` to `MaintenanceRecord` interface
- Add `receiptDocumentId?: string` to `CreateMaintenanceRecordSchema` (Zod, optional UUID)
- Add `receiptDocument?: { id, fileName, contentType, storageKey }` to response type for GET
3. **Repository update**:
- `mapRow()`: add `receiptDocumentId: row.receipt_document_id`
- `create()`: include `receipt_document_id` in INSERT
- `getById()`: LEFT JOIN documents ON maintenance_records.receipt_document_id = documents.id, return file metadata
4. **OCR proxy endpoint**: `POST /api/ocr/extract/maintenance-receipt`
- Tier gate: `canAccessFeature(userTier, 'maintenance.receiptScan')`
- Forward multipart file to OCR microservice `POST http://mvp-ocr:8000/extract/maintenance-receipt`
- Return OCR result to frontend
5. **Feature tier config**: Add `'maintenance.receiptScan': { minTier: 'pro', name: 'Maintenance Receipt Scan', upgradePrompt: 'Upgrade to Pro to scan maintenance receipts' }`
**Acceptance Criteria**: Migration adds FK. Create accepts receiptDocumentId. GET returns receipt metadata. Proxy endpoint works with tier gating.
---
### Milestone 3: Frontend OCR Hook and Review Modal (#152)
**Goal**: Create the maintenance receipt OCR orchestration hook and review modal.
**Files**:
| File | Action | Description |
|------|--------|-------------|
| `frontend/src/features/maintenance/types/maintenance-receipt.types.ts` | NEW | Extraction types |
| `frontend/src/features/maintenance/hooks/useMaintenanceReceiptOcr.ts` | NEW | OCR flow hook |
| `frontend/src/features/maintenance/components/MaintenanceReceiptReviewModal.tsx` | NEW | Review modal |
**Implementation Details**:
1. **Types** (`maintenance-receipt.types.ts`):
```
ExtractedMaintenanceField: { value: string | number | null, confidence: number }
ExtractedMaintenanceFields: { serviceName, serviceDate, totalCost, shopName, laborCost, partsCost, partsBreakdown, odometerReading, vehicleInfo }
MappedMaintenanceFields: { category, subtypes, date, cost, shopName, odometerReading }
MaintenanceReceiptOcrResult: { extractedFields, mappedFields, rawText, documentId }
```
2. **Hook** (`useMaintenanceReceiptOcr.ts`, mirrors `useReceiptOcr`):
- States: idle, capturing, processing, reviewing, accepted, error
- `startCapture()`: opens CameraCapture
- `processImage(file)`: uploads to `/api/ocr/extract/maintenance-receipt`, stores document via documents API
- `acceptResult()`: maps extracted fields to form values:
- `serviceName` -> `category` + `subtypes` via keyword mapping
- `serviceDate` -> `date`
- `totalCost` -> `cost`
- `shopName` -> `shopName`
- `odometerReading` -> `odometerReading`
- Category mapping function: keyword-based mapping of service description to maintenance categories and subtypes (e.g., "oil change" -> routine_maintenance + ["Engine Oil"], "brake pad replacement" -> routine_maintenance + ["Brakes"])
3. **Review Modal** (`MaintenanceReceiptReviewModal.tsx`, mirrors `ReceiptOcrReviewModal`):
- 4-dot confidence indicator per field (LOW_CONFIDENCE_THRESHOLD = 0.7)
- Primary fields (always visible): serviceName, serviceDate, totalCost, shopName
- Secondary fields (collapsible): laborCost, partsCost, odometerReading, vehicleInfo, partsBreakdown
- Inline editing with confidence -> 1.0 on user edit
- Category/subtype suggestion display: show suggested category with option to override
- Mobile: full-width bottom sheet / Desktop: dialog modal
- Receipt preview thumbnail
- Accept/Cancel buttons (44px touch targets on mobile)
**Acceptance Criteria**: Hook manages full OCR flow. Modal displays fields with confidence. Inline editing works. Category suggestion works. Mobile and desktop layouts.
---
### Milestone 4: Form Integration, Tier Gating, Receipt Display (#153)
**Goal**: Wire everything into the maintenance record form and detail view.
**Files**:
| File | Action | Description |
|------|--------|-------------|
| `frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx` | MODIFY | Add scan button, wire OCR hook |
| `frontend/src/features/maintenance/components/MaintenanceRecordDetail.tsx` | MODIFY | Show linked receipt |
| `frontend/src/features/maintenance/api/maintenance.api.ts` | MODIFY | Add receiptDocumentId to create payload |
| `frontend/src/core/config/feature-tiers.ts` | MODIFY | Add maintenance.receiptScan feature key |
**Implementation Details**:
1. **Scan Receipt button** on MaintenanceRecordForm:
- Position: below form header, above fields (mirrors fuel log pattern)
- Tier check: `useTierAccess('maintenance.receiptScan')`
- Pro users: "Scan Receipt" button with camera icon
- Non-Pro users: "Scan Receipt" with lock icon, onClick opens upgrade dialog
- Mobile: full-width button / Desktop: inline button
2. **Form auto-populate** on OCR accept:
- `setValue('category', mappedFields.category)`
- `setValue('subtypes', mappedFields.subtypes)` (triggers subtype reset)
- `setValue('date', mappedFields.date)`
- `setValue('cost', mappedFields.cost)`
- `setValue('shopName', mappedFields.shopName)`
- `setValue('odometerReading', mappedFields.odometerReading)`
- Store `receiptDocumentId` in form state for submission
3. **Create record payload**: Add `receiptDocumentId` to `CreateMaintenanceRecordRequest`
4. **Receipt display** on MaintenanceRecordDetail:
- If `receiptDocument` exists: show thumbnail + "View Receipt" button
- Thumbnail: small image preview (64x64 on mobile, 80x80 on desktop)
- "View Receipt" opens document viewer (existing pattern)
- No receipt: section hidden
**Acceptance Criteria**: Scan button appears (tier-gated). Auto-populate works. Receipt stored and linked. Receipt viewable on detail. Mobile and desktop responsive.
---
### Execution Order
```
M1 (#150) -----> M2 (#151) -----> M3 (#152) -----> M4 (#153)
OCR Pipeline Backend API Frontend Hook Form Integration
(Platform) (Feature) (Frontend) (Frontend)
```
M1 and M2 can potentially run in parallel (OCR microservice is independent of backend). M3 depends on M2 (needs proxy endpoint). M4 depends on M3 (needs hook and modal).
### Risk Assessment
| Risk | Mitigation |
|------|------------|
| Gemini response quality varies | Cross-validation adjusts confidence; user can edit all fields |
| Latency concern (3-6s) | Loading spinner with progress indication; acceptable for Pro feature |
| Category mapping accuracy | Keyword-based with fallback to "Other"; user always reviews suggestion |
| HEIC format support | Already handled by CameraCapture + OCR preprocessor |
---
*Verdict*: AWAITING_REVIEW | *Next*: QR plan-completeness review
[CONSISTENCY] Migration path wrong - Plan says backend/src/features/maintenance/data/migrations/XXX_... but actual path is backend/src/features/maintenance/migrations/ (no data/). Naming is 001_, 002_, 003_ sequential, not timestamp.
[CONSISTENCY] Proxy route architecture wrong - Plan puts proxy in maintenance.routes.ts but all OCR proxy endpoints live in backend/src/features/ocr/. Missing full chain: ocr.routes.ts -> ocr.controller.ts -> ocr.service.ts -> ocr-client.ts -> ocr.types.ts.
[TEMPORAL] MaintenanceRecordDetail.tsx does not exist - Referenced as MODIFY but file is not in codebase.
MEDIUM:
4. Tier gating should use requireTier() preHandler pattern (not canAccessFeature inline check)
5. Clarify M1 extractor reuses existing ReceiptExtractionResult dataclass (generic dict)
6. Clarify M1 reuses existing ReceiptExtractionResponse Pydantic model
7. Explicitly state frontend owns category mapping, OCR returns raw serviceName
8. Clarify document storage ordering in hook (OCR first, then store doc on success)
9. Clarify FK cascade behavior in both directions
LOW:
10. Consolidate category mapping description (redundant across M3/M4)
BONUS:
11. Rename maintenance_receipt_patterns.py to avoid collision with existing maintenance_patterns.py - suggest maintenance_receipt_validation.py
Naming Consistency: All primary names consistent across plan and sub-issues. No mismatches in component/hook/endpoint/feature key names.
Verdict: NEEDS_REVISION | Next: Address HIGH findings, re-scrub
## TW Review: plan-scrub
**Phase**: Plan-Review | **Agent**: Technical Writer | **Status**: NEEDS_REVISION
### Findings (11 items: 3 HIGH, 5 MEDIUM, 1 LOW, 2 BONUS)
**HIGH:**
1. **[CONSISTENCY] Migration path wrong** - Plan says `backend/src/features/maintenance/data/migrations/XXX_...` but actual path is `backend/src/features/maintenance/migrations/` (no `data/`). Naming is `001_`, `002_`, `003_` sequential, not timestamp.
2. **[CONSISTENCY] Proxy route architecture wrong** - Plan puts proxy in `maintenance.routes.ts` but all OCR proxy endpoints live in `backend/src/features/ocr/`. Missing full chain: `ocr.routes.ts` -> `ocr.controller.ts` -> `ocr.service.ts` -> `ocr-client.ts` -> `ocr.types.ts`.
3. **[TEMPORAL] `MaintenanceRecordDetail.tsx` does not exist** - Referenced as MODIFY but file is not in codebase.
**MEDIUM:**
4. Tier gating should use `requireTier()` preHandler pattern (not `canAccessFeature` inline check)
5. Clarify M1 extractor reuses existing `ReceiptExtractionResult` dataclass (generic dict)
6. Clarify M1 reuses existing `ReceiptExtractionResponse` Pydantic model
7. Explicitly state frontend owns category mapping, OCR returns raw `serviceName`
8. Clarify document storage ordering in hook (OCR first, then store doc on success)
9. Clarify FK cascade behavior in both directions
**LOW:**
10. Consolidate category mapping description (redundant across M3/M4)
**BONUS:**
11. Rename `maintenance_receipt_patterns.py` to avoid collision with existing `maintenance_patterns.py` - suggest `maintenance_receipt_validation.py`
### Naming Consistency: All primary names consistent across plan and sub-issues. No mismatches in component/hook/endpoint/feature key names.
*Verdict*: NEEDS_REVISION | *Next*: Address HIGH findings, re-scrub
Pass through receiptDocumentId, return receipt metadata
backend/src/features/ocr/api/ocr.routes.ts
MODIFY
Add POST /ocr/extract/maintenance-receipt route with requireTier preHandler
backend/src/features/ocr/api/ocr.controller.ts
MODIFY
Add extractMaintenanceReceipt handler
backend/src/features/ocr/domain/ocr.service.ts
MODIFY
Add extractMaintenanceReceipt method
backend/src/features/ocr/domain/ocr.types.ts
MODIFY
Add maintenance receipt response types
backend/src/features/ocr/external/ocr-client.ts
MODIFY
Add extractMaintenanceReceipt method (HTTP call to OCR microservice)
backend/src/core/config/feature-tiers.ts
MODIFY
Add maintenance.receiptScan feature key
3. MaintenanceRecordDetail.tsx resolved
No dedicated detail view exists. Receipt display goes into two existing components:
MaintenanceRecordEditDialog.tsx (MODIFY) - Show receipt thumbnail + "View Receipt" button when record has linked receipt (in the dialog content area, above or below the form fields)
MaintenanceRecordsList.tsx (MODIFY) - Show receipt indicator icon on list rows for records that have linked receipts
MaintenanceReceiptExtractor.extract() returns the existing ReceiptExtractionResult dataclass from receipt_extractor.py. The extracted_fields: dict[str, ExtractedField] structure already supports arbitrary field names. Maintenance fields use keys: serviceName, serviceDate, totalCost, shopName, laborCost, partsCost, partsBreakdown, odometerReading, vehicleInfo. No new result dataclass needed.
6. M1 reuses existing ReceiptExtractionResponse Pydantic model
The existing ReceiptExtractionResponse model supports this without modification (response shape is identical, field keys differ). Remove ocr/app/models/ from M1 files table. The receiptType field will be "maintenance".
Regex cross-validation for dates, amounts, odometer
ocr/app/routers/extract.py
MODIFY
Add POST /extract/maintenance-receipt route
7. Category mapping ownership
Design decision: The OCR endpoint returns the raw serviceName string as extracted by Gemini. The frontend category-mapper.ts independently maps this to a category and subtypes, because the user reviews and can override the suggestion before submission. The OCR microservice has no knowledge of frontend maintenance categories.
8. Document storage ordering in hook
Flow in useMaintenanceReceiptOcr.processImage(file):
Upload file to POST /api/ocr/extract/maintenance-receipt for extraction
On OCR success: upload the same file to documents API (POST /api/documents) to get a documentId
Store both OCR result and documentId in hook state for review step
If OCR fails: no document is stored (document upload only happens on OCR success)
If document upload fails after OCR success: show error toast, preserve OCR result, allow retry of document upload only
9. FK cascade behavior (both directions)
Document deleted: FK on maintenance_records set to NULL (ON DELETE SET NULL)
Maintenance record deleted: document is preserved (FK is on maintenance_records, deleting the record simply removes the FK reference; no cascade needed)
LOW Fix
10. Category mapping consolidated to M3 only
All category mapping description owned by M3. M4 simply references "use mappedFields from the hook's acceptResult() callback" without re-describing the mapping logic.
BONUS Fix
11. Patterns file renamed
maintenance_receipt_patterns.py renamed to maintenance_receipt_validation.py to avoid collision with existing maintenance_patterns.py.
Verdict: ALL_FINDINGS_ADDRESSED | Next: QR plan-code review
## Plan Revision: Addressing TW plan-scrub Findings
**Phase**: Planning | **Agent**: Planner | **Status**: IN_PROGRESS
All 11 TW findings addressed below. This revision supersedes conflicting details in the original plan.
---
### HIGH Fixes
**1. Migration path corrected**
Correct path: `backend/src/features/maintenance/migrations/004_add_receipt_document_id.sql`
- No `data/` segment
- Sequential `004_` prefix (follows existing `001_`, `002_`, `003_`)
**2. Proxy route architecture corrected**
The OCR proxy endpoint lives entirely in the `ocr` feature, not `maintenance`. M2 files table revised:
| File | Action | Description |
|------|--------|-------------|
| `backend/src/features/maintenance/migrations/004_add_receipt_document_id.sql` | NEW | Migration: add receipt_document_id FK |
| `backend/src/features/maintenance/domain/maintenance.types.ts` | MODIFY | Add receiptDocumentId to types/schemas |
| `backend/src/features/maintenance/data/maintenance.repository.ts` | MODIFY | Update mapRow, create/get queries |
| `backend/src/features/maintenance/domain/maintenance.service.ts` | MODIFY | Accept receiptDocumentId on create |
| `backend/src/features/maintenance/api/maintenance.controller.ts` | MODIFY | Pass through receiptDocumentId, return receipt metadata |
| `backend/src/features/ocr/api/ocr.routes.ts` | MODIFY | Add `POST /ocr/extract/maintenance-receipt` route with `requireTier` preHandler |
| `backend/src/features/ocr/api/ocr.controller.ts` | MODIFY | Add `extractMaintenanceReceipt` handler |
| `backend/src/features/ocr/domain/ocr.service.ts` | MODIFY | Add `extractMaintenanceReceipt` method |
| `backend/src/features/ocr/domain/ocr.types.ts` | MODIFY | Add maintenance receipt response types |
| `backend/src/features/ocr/external/ocr-client.ts` | MODIFY | Add `extractMaintenanceReceipt` method (HTTP call to OCR microservice) |
| `backend/src/core/config/feature-tiers.ts` | MODIFY | Add `maintenance.receiptScan` feature key |
**3. `MaintenanceRecordDetail.tsx` resolved**
No dedicated detail view exists. Receipt display goes into two existing components:
- `MaintenanceRecordEditDialog.tsx` (MODIFY) - Show receipt thumbnail + "View Receipt" button when record has linked receipt (in the dialog content area, above or below the form fields)
- `MaintenanceRecordsList.tsx` (MODIFY) - Show receipt indicator icon on list rows for records that have linked receipts
M4 files table revised:
| File | Action | Description |
|------|--------|-------------|
| `frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx` | MODIFY | Add scan button, wire OCR hook, auto-populate |
| `frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx` | MODIFY | Show linked receipt thumbnail + view button |
| `frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx` | MODIFY | Show receipt indicator on list rows |
| `frontend/src/features/maintenance/api/maintenance.api.ts` | MODIFY | Add receiptDocumentId to create payload |
| `frontend/src/core/config/feature-tiers.ts` | MODIFY | Add maintenance.receiptScan feature key |
---
### MEDIUM Fixes
**4. Tier gating pattern**
Route preHandler pattern (matching existing OCR routes):
```
preHandler: [requireAuth, requireTier('maintenance.receiptScan')]
```
NOT `canAccessFeature()` inline check. Follows `ocr.routes.ts` line 29 pattern.
**5. M1 extractor reuses existing `ReceiptExtractionResult`**
`MaintenanceReceiptExtractor.extract()` returns the existing `ReceiptExtractionResult` dataclass from `receipt_extractor.py`. The `extracted_fields: dict[str, ExtractedField]` structure already supports arbitrary field names. Maintenance fields use keys: `serviceName`, `serviceDate`, `totalCost`, `shopName`, `laborCost`, `partsCost`, `partsBreakdown`, `odometerReading`, `vehicleInfo`. No new result dataclass needed.
**6. M1 reuses existing `ReceiptExtractionResponse` Pydantic model**
The existing `ReceiptExtractionResponse` model supports this without modification (response shape is identical, field keys differ). Remove `ocr/app/models/` from M1 files table. The `receiptType` field will be `"maintenance"`.
M1 files table revised:
| File | Action | Description |
|------|--------|-------------|
| `ocr/app/extractors/maintenance_receipt_extractor.py` | NEW | MaintenanceReceiptExtractor class |
| `ocr/app/patterns/maintenance_receipt_validation.py` | NEW | Regex cross-validation for dates, amounts, odometer |
| `ocr/app/routers/extract.py` | MODIFY | Add `POST /extract/maintenance-receipt` route |
**7. Category mapping ownership**
Design decision: The OCR endpoint returns the raw `serviceName` string as extracted by Gemini. The frontend `category-mapper.ts` independently maps this to a category and subtypes, because the user reviews and can override the suggestion before submission. The OCR microservice has no knowledge of frontend maintenance categories.
**8. Document storage ordering in hook**
Flow in `useMaintenanceReceiptOcr.processImage(file)`:
1. Upload file to `POST /api/ocr/extract/maintenance-receipt` for extraction
2. On OCR success: upload the same file to documents API (`POST /api/documents`) to get a `documentId`
3. Store both OCR result and `documentId` in hook state for review step
4. If OCR fails: no document is stored (document upload only happens on OCR success)
5. If document upload fails after OCR success: show error toast, preserve OCR result, allow retry of document upload only
**9. FK cascade behavior (both directions)**
- **Document deleted**: FK on maintenance_records set to NULL (`ON DELETE SET NULL`)
- **Maintenance record deleted**: document is preserved (FK is on maintenance_records, deleting the record simply removes the FK reference; no cascade needed)
---
### LOW Fix
**10. Category mapping consolidated to M3 only**
All category mapping description owned by M3. M4 simply references "use `mappedFields` from the hook's `acceptResult()` callback" without re-describing the mapping logic.
---
### BONUS Fix
**11. Patterns file renamed**
`maintenance_receipt_patterns.py` renamed to `maintenance_receipt_validation.py` to avoid collision with existing `maintenance_patterns.py`.
---
*Verdict*: ALL_FINDINGS_ADDRESSED | *Next*: QR plan-code review
Catch Gemini 429 responses with exponential backoff: initial 1s, max 3 retries, backoff multiplier 2x
On exhausted retries, return ReceiptExtractionResult(success=False, error="OCR_RATE_LIMITED")
Backend proxy propagates error code to frontend
Frontend shows: "Service temporarily busy. Please try again in a moment." toast with retry button
2. Parameterized Queries (M2)
All repository queries involving receipt_document_id MUST use parameterized placeholders ($N). This is already the established pattern in the repository (all existing queries use $1, $2, ... placeholders). Explicit statement for developer agents:
INSERT: INSERT INTO maintenance_records (..., receipt_document_id) VALUES (..., $N)
SELECT with JOIN: LEFT JOIN documents d ON mr.receipt_document_id = d.id WHERE mr.id = $1
No string concatenation. Ever.
3. Mobile Secondary Fields Interaction (M3)
Review modal secondary fields on mobile:
Use MUI Accordion component for collapsible secondary fields section
Each field row inside accordion maintains 44px min-height
SHOULD_FIX Acknowledgments
4. Zod UUID Validation (M2)
CreateMaintenanceRecordSchema field: receiptDocumentId: z.string().uuid().optional(). Service layer validates document exists and belongs to user with a documentsRepository.getById() check before insertion. Return 404 if document not found, 403 if wrong user.
Both FuelReceiptExtractor and MaintenanceReceiptExtractor import from this shared module.
Verdict: ALL_HIGH_FINDINGS_ADDRESSED | Next: QR plan-docs (final gate)
## Plan Addendum: Addressing QR plan-code Findings
**Phase**: Planning | **Agent**: Planner | **Status**: IN_PROGRESS
### HIGH Fixes
**1. Gemini API Rate Limits (M1)**
Add to `MaintenanceReceiptExtractor.extract()`:
- Catch Gemini 429 responses with exponential backoff: initial 1s, max 3 retries, backoff multiplier 2x
- On exhausted retries, return `ReceiptExtractionResult(success=False, error="OCR_RATE_LIMITED")`
- Backend proxy propagates error code to frontend
- Frontend shows: "Service temporarily busy. Please try again in a moment." toast with retry button
**2. Parameterized Queries (M2)**
All repository queries involving `receipt_document_id` MUST use parameterized placeholders (`$N`). This is already the established pattern in the repository (all existing queries use `$1, $2, ...` placeholders). Explicit statement for developer agents:
- INSERT: `INSERT INTO maintenance_records (..., receipt_document_id) VALUES (..., $N)`
- SELECT with JOIN: `LEFT JOIN documents d ON mr.receipt_document_id = d.id WHERE mr.id = $1`
- No string concatenation. Ever.
**3. Mobile Secondary Fields Interaction (M3)**
Review modal secondary fields on mobile:
- Use MUI `Accordion` component for collapsible secondary fields section
- Accordion summary: "Additional Details" label, 44px min-height touch target
- Collapsed by default on mobile (< 768px), expanded by default on desktop
- Fields inside accordion: laborCost, partsCost, odometerReading, vehicleInfo, partsBreakdown
- Each field row inside accordion maintains 44px min-height
### SHOULD_FIX Acknowledgments
**4. Zod UUID Validation (M2)**
`CreateMaintenanceRecordSchema` field: `receiptDocumentId: z.string().uuid().optional()`. Service layer validates document exists and belongs to user with a `documentsRepository.getById()` check before insertion. Return 404 if document not found, 403 if wrong user.
**5. Confidence Constants (M1)**
Create shared module `ocr/app/patterns/confidence.py`:
```
CONFIDENCE_BOOST = 1.1 # Regex confirms Gemini value
CONFIDENCE_PENALTY = 0.8 # Regex contradicts Gemini value
GEMINI_DEFAULT = 0.85 # Default Gemini extraction confidence
```
Both `FuelReceiptExtractor` and `MaintenanceReceiptExtractor` import from this shared module.
---
*Verdict*: ALL_HIGH_FINDINGS_ADDRESSED | *Next*: QR plan-docs (final gate)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Add receipt upload and camera capture to the maintenance record form. OCR extracts maintenance-specific fields (service name, date, cost, shop name, odometer) and auto-populates the form. Receipts are stored as documents and linked to the maintenance record.
This mirrors the existing fuel receipt OCR flow (
ReceiptOcrReviewModal) but targets maintenance-specific field extraction rather than fuel-specific fields.Requirements
Receipt Upload on Maintenance Record Form
MaintenanceRecordForm(mirrors fuel log pattern)CameraCapturecomponentmaintenance.receiptScanfeature flag)OCR Extraction for Maintenance Receipts
Review Modal
MaintenanceReceiptReviewModalcomponent (mirrorsReceiptOcrReviewModal)Auto-populate Maintenance Record Form
MaintenanceRecordFormfields viasetValue()Document Storage and Linking
receipt_document_id(nullable FK) tomaintenance_recordstableBackend API
POST /api/ocr/extract/maintenance-receiptPOST /api/maintenance/recordsto accept optionalreceiptDocumentIdGET /api/maintenance/records/:idto include receipt document metadataTechnical Considerations
CameraCapture, confidence indicator, and review modal patterns from fuel logsuseMaintenanceReceiptOcrhook (mirrorsuseReceiptOcr)useTierAccess('maintenance.receiptScan')Dependencies
Acceptance Criteria
Email Receipt Ingestion via Resend APIto feat: Email Receipt Ingestion via Resend APIfeat: Email Receipt Ingestion via Resend APIto feat: Maintenance Receipt Upload with OCR Auto-populatePlan: Maintenance Receipt Upload with OCR Auto-populate
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Decision Critic Result
Decision: OCR extraction approach for maintenance receipts.
Verdict: REVISE to Gemini-primary extraction with regex cross-validation.
FuelReceiptExtractorcross-validation pattern (extract first, validate second)Sub-Issue Decomposition
Branch:
issue-16-maintenance-receipt-ocr(ONE branch for all milestones)PR: ONE PR targeting main, closes #16, #150, #151, #152, #153
Milestone 1: OCR Microservice Extraction Pipeline (#150)
Goal: Add maintenance receipt extraction endpoint to Python OCR microservice.
Files:
ocr/app/extractors/maintenance_receipt_extractor.pyocr/app/patterns/maintenance_receipt_patterns.pyocr/app/routers/extract.pyPOST /extract/maintenance-receiptrouteocr/app/models/Implementation Details:
MaintenanceReceiptExtractor (mirrors
FuelReceiptExtractor):ReceiptExtractorand Gemini moduleextract(image_bytes, content_type)method:a. Call base
ReceiptExtractor.extract()to get OCR text via preprocessing pipelineb. Send OCR text to Gemini with structured prompt requesting JSON output:
serviceName(string) - service performedserviceDate(string, YYYY-MM-DD)totalCost(number)shopName(string)laborCost(number, optional)partsCost(number, optional)partsBreakdown(list of {name, cost}, optional)odometerReading(number, optional)vehicleInfo(string, optional)c. Cross-validate with regex:
date_matcherfor serviceDate,currency_matcherfor costs, numeric pattern for odometerd. Adjust confidence: boost 1.1x if regex confirms Gemini value, reduce 0.8x if mismatch
e. Return
ReceiptExtractionResultwith per-fieldExtractedField(value, confidence)Cross-validation patterns (
maintenance_receipt_patterns.py):/(\d{1,3}[,.]?\d{3})\s*(mi|miles|km|odometer)/iFastAPI endpoint:
POST /extract/maintenance-receipt{ success, receiptType: "maintenance", extractedFields: { fieldName: { value, confidence } }, rawText, processingTimeMs }Acceptance Criteria: Endpoint returns structured fields with confidence scores. Gemini extracts semantic fields. Regex cross-validates structured fields.
Milestone 2: Backend Migration and API Updates (#151)
Goal: Add receipt document linking to maintenance records and OCR proxy endpoint.
Files:
backend/src/features/maintenance/data/migrations/XXX_add_receipt_document_id.sqlbackend/src/features/maintenance/domain/maintenance.types.tsbackend/src/features/maintenance/data/maintenance.repository.tsbackend/src/features/maintenance/domain/maintenance.service.tsbackend/src/features/maintenance/api/maintenance.controller.tsbackend/src/features/maintenance/api/maintenance.routes.tsbackend/src/features/ocr/api/ocr.controller.tsbackend/src/core/config/feature-tiers.tsImplementation Details:
Migration:
ALTER TABLE maintenance_records ADD COLUMN receipt_document_id UUID REFERENCES documents(id) ON DELETE SET NULL;Types update:
receiptDocumentId?: stringtoMaintenanceRecordinterfacereceiptDocumentId?: stringtoCreateMaintenanceRecordSchema(Zod, optional UUID)receiptDocument?: { id, fileName, contentType, storageKey }to response type for GETRepository update:
mapRow(): addreceiptDocumentId: row.receipt_document_idcreate(): includereceipt_document_idin INSERTgetById(): LEFT JOIN documents ON maintenance_records.receipt_document_id = documents.id, return file metadataOCR proxy endpoint:
POST /api/ocr/extract/maintenance-receiptcanAccessFeature(userTier, 'maintenance.receiptScan')POST http://mvp-ocr:8000/extract/maintenance-receiptFeature tier config: Add
'maintenance.receiptScan': { minTier: 'pro', name: 'Maintenance Receipt Scan', upgradePrompt: 'Upgrade to Pro to scan maintenance receipts' }Acceptance Criteria: Migration adds FK. Create accepts receiptDocumentId. GET returns receipt metadata. Proxy endpoint works with tier gating.
Milestone 3: Frontend OCR Hook and Review Modal (#152)
Goal: Create the maintenance receipt OCR orchestration hook and review modal.
Files:
frontend/src/features/maintenance/types/maintenance-receipt.types.tsfrontend/src/features/maintenance/hooks/useMaintenanceReceiptOcr.tsfrontend/src/features/maintenance/components/MaintenanceReceiptReviewModal.tsxImplementation Details:
Types (
maintenance-receipt.types.ts):Hook (
useMaintenanceReceiptOcr.ts, mirrorsuseReceiptOcr):startCapture(): opens CameraCaptureprocessImage(file): uploads to/api/ocr/extract/maintenance-receipt, stores document via documents APIacceptResult(): maps extracted fields to form values:serviceName->category+subtypesvia keyword mappingserviceDate->datetotalCost->costshopName->shopNameodometerReading->odometerReadingReview Modal (
MaintenanceReceiptReviewModal.tsx, mirrorsReceiptOcrReviewModal):Acceptance Criteria: Hook manages full OCR flow. Modal displays fields with confidence. Inline editing works. Category suggestion works. Mobile and desktop layouts.
Milestone 4: Form Integration, Tier Gating, Receipt Display (#153)
Goal: Wire everything into the maintenance record form and detail view.
Files:
frontend/src/features/maintenance/components/MaintenanceRecordForm.tsxfrontend/src/features/maintenance/components/MaintenanceRecordDetail.tsxfrontend/src/features/maintenance/api/maintenance.api.tsfrontend/src/core/config/feature-tiers.tsImplementation Details:
Scan Receipt button on MaintenanceRecordForm:
useTierAccess('maintenance.receiptScan')Form auto-populate on OCR accept:
setValue('category', mappedFields.category)setValue('subtypes', mappedFields.subtypes)(triggers subtype reset)setValue('date', mappedFields.date)setValue('cost', mappedFields.cost)setValue('shopName', mappedFields.shopName)setValue('odometerReading', mappedFields.odometerReading)receiptDocumentIdin form state for submissionCreate record payload: Add
receiptDocumentIdtoCreateMaintenanceRecordRequestReceipt display on MaintenanceRecordDetail:
receiptDocumentexists: show thumbnail + "View Receipt" buttonAcceptance Criteria: Scan button appears (tier-gated). Auto-populate works. Receipt stored and linked. Receipt viewable on detail. Mobile and desktop responsive.
Execution Order
M1 and M2 can potentially run in parallel (OCR microservice is independent of backend). M3 depends on M2 (needs proxy endpoint). M4 depends on M3 (needs hook and modal).
Risk Assessment
Verdict: AWAITING_REVIEW | Next: QR plan-completeness review
QR Review: plan-completeness
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS_WITH_CONCERNS
VERDICT: PASS_WITH_CONCERNS
Findings
[RULE 1] [SHOULD_FIX]: Missing Policy Defaults for Error Handling
[RULE 1] [SHOULD_FIX]: Incomplete File Manifest for M3
frontend/src/features/maintenance/utils/category-mapper.tsto M3 files[RULE 1] [SHOULD_FIX]: Mobile/Desktop Behavior Not Explicit for Receipt Display
[RULE 2] [SHOULD_FIX]: Test Strategy Not Documented
[RULE 2] [SHOULD_FIX]: Migration Filename Pattern Not Specified
XXX_add_receipt_document_id.sqlIssue Acceptance Criteria Coverage: 10/10 covered
Verdict: PASS_WITH_CONCERNS | Next: Address concerns, then TW plan-scrub
Plan Addendum: Addressing QR plan-completeness Concerns
Phase: Planning | Agent: Planner | Status: IN_PROGRESS
1. Policy Defaults (Error Handling)
{ error: "OCR_UNAVAILABLE" }, frontend shows retry toast2. M3 File Manifest Addition
Add to Milestone 3 files:
frontend/src/features/maintenance/utils/category-mapper.tsCategory mapper exports:
mapServiceToCategory(serviceName: string): { category: MaintenanceCategory, subtypes: string[] }with keyword lookup table.3. M4 Receipt Display Mobile/Desktop Specs
Mobile (< 768px):
Desktop (>= 768px):
4. Testing Strategy
MaintenanceReceiptExtractorwith sample receipt text, cross-validation edge cases (mismatched dates, invalid amounts), Gemini response parsingMaintenanceReceiptReviewModal(render, edit, confidence display), hook state machine transitions5. Migration Filename Convention
Use timestamp format matching existing migrations:
{YYYYMMDDHHMMSS}_add_receipt_document_id.sqlExample:
20260213000000_add_receipt_document_id.sqlVerdict: CONCERNS_ADDRESSED | Next: TW plan-scrub
TW Review: plan-scrub
Phase: Plan-Review | Agent: Technical Writer | Status: NEEDS_REVISION
Findings (11 items: 3 HIGH, 5 MEDIUM, 1 LOW, 2 BONUS)
HIGH:
backend/src/features/maintenance/data/migrations/XXX_...but actual path isbackend/src/features/maintenance/migrations/(nodata/). Naming is001_,002_,003_sequential, not timestamp.maintenance.routes.tsbut all OCR proxy endpoints live inbackend/src/features/ocr/. Missing full chain:ocr.routes.ts->ocr.controller.ts->ocr.service.ts->ocr-client.ts->ocr.types.ts.MaintenanceRecordDetail.tsxdoes not exist - Referenced as MODIFY but file is not in codebase.MEDIUM:
4. Tier gating should use
requireTier()preHandler pattern (notcanAccessFeatureinline check)5. Clarify M1 extractor reuses existing
ReceiptExtractionResultdataclass (generic dict)6. Clarify M1 reuses existing
ReceiptExtractionResponsePydantic model7. Explicitly state frontend owns category mapping, OCR returns raw
serviceName8. Clarify document storage ordering in hook (OCR first, then store doc on success)
9. Clarify FK cascade behavior in both directions
LOW:
10. Consolidate category mapping description (redundant across M3/M4)
BONUS:
11. Rename
maintenance_receipt_patterns.pyto avoid collision with existingmaintenance_patterns.py- suggestmaintenance_receipt_validation.pyNaming Consistency: All primary names consistent across plan and sub-issues. No mismatches in component/hook/endpoint/feature key names.
Verdict: NEEDS_REVISION | Next: Address HIGH findings, re-scrub
Plan Revision: Addressing TW plan-scrub Findings
Phase: Planning | Agent: Planner | Status: IN_PROGRESS
All 11 TW findings addressed below. This revision supersedes conflicting details in the original plan.
HIGH Fixes
1. Migration path corrected
Correct path:
backend/src/features/maintenance/migrations/004_add_receipt_document_id.sqldata/segment004_prefix (follows existing001_,002_,003_)2. Proxy route architecture corrected
The OCR proxy endpoint lives entirely in the
ocrfeature, notmaintenance. M2 files table revised:backend/src/features/maintenance/migrations/004_add_receipt_document_id.sqlbackend/src/features/maintenance/domain/maintenance.types.tsbackend/src/features/maintenance/data/maintenance.repository.tsbackend/src/features/maintenance/domain/maintenance.service.tsbackend/src/features/maintenance/api/maintenance.controller.tsbackend/src/features/ocr/api/ocr.routes.tsPOST /ocr/extract/maintenance-receiptroute withrequireTierpreHandlerbackend/src/features/ocr/api/ocr.controller.tsextractMaintenanceReceipthandlerbackend/src/features/ocr/domain/ocr.service.tsextractMaintenanceReceiptmethodbackend/src/features/ocr/domain/ocr.types.tsbackend/src/features/ocr/external/ocr-client.tsextractMaintenanceReceiptmethod (HTTP call to OCR microservice)backend/src/core/config/feature-tiers.tsmaintenance.receiptScanfeature key3.
MaintenanceRecordDetail.tsxresolvedNo dedicated detail view exists. Receipt display goes into two existing components:
MaintenanceRecordEditDialog.tsx(MODIFY) - Show receipt thumbnail + "View Receipt" button when record has linked receipt (in the dialog content area, above or below the form fields)MaintenanceRecordsList.tsx(MODIFY) - Show receipt indicator icon on list rows for records that have linked receiptsM4 files table revised:
frontend/src/features/maintenance/components/MaintenanceRecordForm.tsxfrontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsxfrontend/src/features/maintenance/components/MaintenanceRecordsList.tsxfrontend/src/features/maintenance/api/maintenance.api.tsfrontend/src/core/config/feature-tiers.tsMEDIUM Fixes
4. Tier gating pattern
Route preHandler pattern (matching existing OCR routes):
NOT
canAccessFeature()inline check. Followsocr.routes.tsline 29 pattern.5. M1 extractor reuses existing
ReceiptExtractionResultMaintenanceReceiptExtractor.extract()returns the existingReceiptExtractionResultdataclass fromreceipt_extractor.py. Theextracted_fields: dict[str, ExtractedField]structure already supports arbitrary field names. Maintenance fields use keys:serviceName,serviceDate,totalCost,shopName,laborCost,partsCost,partsBreakdown,odometerReading,vehicleInfo. No new result dataclass needed.6. M1 reuses existing
ReceiptExtractionResponsePydantic modelThe existing
ReceiptExtractionResponsemodel supports this without modification (response shape is identical, field keys differ). Removeocr/app/models/from M1 files table. ThereceiptTypefield will be"maintenance".M1 files table revised:
ocr/app/extractors/maintenance_receipt_extractor.pyocr/app/patterns/maintenance_receipt_validation.pyocr/app/routers/extract.pyPOST /extract/maintenance-receiptroute7. Category mapping ownership
Design decision: The OCR endpoint returns the raw
serviceNamestring as extracted by Gemini. The frontendcategory-mapper.tsindependently maps this to a category and subtypes, because the user reviews and can override the suggestion before submission. The OCR microservice has no knowledge of frontend maintenance categories.8. Document storage ordering in hook
Flow in
useMaintenanceReceiptOcr.processImage(file):POST /api/ocr/extract/maintenance-receiptfor extractionPOST /api/documents) to get adocumentIddocumentIdin hook state for review step9. FK cascade behavior (both directions)
ON DELETE SET NULL)LOW Fix
10. Category mapping consolidated to M3 only
All category mapping description owned by M3. M4 simply references "use
mappedFieldsfrom the hook'sacceptResult()callback" without re-describing the mapping logic.BONUS Fix
11. Patterns file renamed
maintenance_receipt_patterns.pyrenamed tomaintenance_receipt_validation.pyto avoid collision with existingmaintenance_patterns.py.Verdict: ALL_FINDINGS_ADDRESSED | Next: QR plan-code review
QR Review: plan-code
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS_WITH_CONCERNS
VERDICT: PASS_WITH_CONCERNS
Findings
[RULE 0] [HIGH]: Missing Error Handling for Gemini API Rate Limits
OCR_RATE_LIMITEDerror code[RULE 0] [HIGH]: SQL Injection Risk via receiptDocumentId
[RULE 1] [HIGH]: Mobile Interaction for Review Modal Secondary Fields
[RULE 0] [SHOULD_FIX]: File Upload MIME Type Verification
ReceiptExtractor._detect_mime_type()already uses python-magic. New extractor inherits this. Acknowledged.[RULE 1] [SHOULD_FIX]: receiptDocumentId Zod UUID Validation
.uuid()in Zod. Service layer validates document exists and belongs to user.[RULE 2] [SHOULD_FIX]: Confidence Constants Duplication
app/patterns/confidence.py(CONFIDENCE_BOOST = 1.1, CONFIDENCE_PENALTY = 0.8)[RULE 2] [SUGGESTION]: Category Mapper Maintainability
Considered But Not Flagged
Verdict: PASS_WITH_CONCERNS | Next: Address HIGH findings, then QR plan-docs
Plan Addendum: Addressing QR plan-code Findings
Phase: Planning | Agent: Planner | Status: IN_PROGRESS
HIGH Fixes
1. Gemini API Rate Limits (M1)
Add to
MaintenanceReceiptExtractor.extract():ReceiptExtractionResult(success=False, error="OCR_RATE_LIMITED")2. Parameterized Queries (M2)
All repository queries involving
receipt_document_idMUST use parameterized placeholders ($N). This is already the established pattern in the repository (all existing queries use$1, $2, ...placeholders). Explicit statement for developer agents:INSERT INTO maintenance_records (..., receipt_document_id) VALUES (..., $N)LEFT JOIN documents d ON mr.receipt_document_id = d.id WHERE mr.id = $13. Mobile Secondary Fields Interaction (M3)
Review modal secondary fields on mobile:
Accordioncomponent for collapsible secondary fields sectionSHOULD_FIX Acknowledgments
4. Zod UUID Validation (M2)
CreateMaintenanceRecordSchemafield:receiptDocumentId: z.string().uuid().optional(). Service layer validates document exists and belongs to user with adocumentsRepository.getById()check before insertion. Return 404 if document not found, 403 if wrong user.5. Confidence Constants (M1)
Create shared module
ocr/app/patterns/confidence.py:Both
FuelReceiptExtractorandMaintenanceReceiptExtractorimport from this shared module.Verdict: ALL_HIGH_FINDINGS_ADDRESSED | Next: QR plan-docs (final gate)