feat: Maintenance Receipt Upload with OCR Auto-populate #16

Closed
opened 2026-01-04 05:17:27 +00:00 by egullickson · 7 comments
Owner

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
## 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 added the
status
backlog
type
feature
labels 2026-01-04 05:18:34 +00:00
egullickson changed title from Email Receipt Ingestion via Resend API to feat: Email Receipt Ingestion via Resend API 2026-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-populate 2026-02-13 02:45:59 +00:00
egullickson added
status
in-progress
and removed
status
backlog
labels 2026-02-13 02:48:49 +00:00
egullickson modified the milestone from Sprint 2026-01-05 to Sprint 2026-02-02 2026-02-13 02:49:05 +00:00
Author
Owner

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

## 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
Author
Owner

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

  • Location: All milestones
  • Issue: Plan does not explicitly state error handling policies for OCR failures, network timeouts, or document upload failures
  • Suggested Fix: Add "Policy Defaults" section with error handling, timeout, and retry behavior

[RULE 1] [SHOULD_FIX]: Incomplete File Manifest for M3

  • Location: Milestone 3
  • Issue: Category mapping function described but no specific file location for the keyword mapping logic
  • Suggested Fix: Add frontend/src/features/maintenance/utils/category-mapper.ts to M3 files

[RULE 1] [SHOULD_FIX]: Mobile/Desktop Behavior Not Explicit for Receipt Display

  • Location: Milestone 4
  • Issue: MaintenanceRecordDetail receipt display does not specify mobile-specific behavior
  • Suggested Fix: Specify mobile: 64x64 thumbnail + full-width button vs desktop: 80x80 + inline button

[RULE 2] [SHOULD_FIX]: Test Strategy Not Documented

  • Location: Plan-level
  • Issue: No mention of testing approach
  • Suggested Fix: Add testing strategy per milestone

[RULE 2] [SHOULD_FIX]: Migration Filename Pattern Not Specified

  • Location: M2 files table
  • Issue: Uses placeholder XXX_add_receipt_document_id.sql
  • Suggested Fix: Specify naming convention or use actual timestamp

Issue Acceptance Criteria Coverage: 10/10 covered

Verdict: PASS_WITH_CONCERNS | Next: Address concerns, then TW plan-scrub

## 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** - **Location**: All milestones - **Issue**: Plan does not explicitly state error handling policies for OCR failures, network timeouts, or document upload failures - **Suggested Fix**: Add "Policy Defaults" section with error handling, timeout, and retry behavior **[RULE 1] [SHOULD_FIX]: Incomplete File Manifest for M3** - **Location**: Milestone 3 - **Issue**: Category mapping function described but no specific file location for the keyword mapping logic - **Suggested Fix**: Add `frontend/src/features/maintenance/utils/category-mapper.ts` to M3 files **[RULE 1] [SHOULD_FIX]: Mobile/Desktop Behavior Not Explicit for Receipt Display** - **Location**: Milestone 4 - **Issue**: MaintenanceRecordDetail receipt display does not specify mobile-specific behavior - **Suggested Fix**: Specify mobile: 64x64 thumbnail + full-width button vs desktop: 80x80 + inline button **[RULE 2] [SHOULD_FIX]: Test Strategy Not Documented** - **Location**: Plan-level - **Issue**: No mention of testing approach - **Suggested Fix**: Add testing strategy per milestone **[RULE 2] [SHOULD_FIX]: Migration Filename Pattern Not Specified** - **Location**: M2 files table - **Issue**: Uses placeholder `XXX_add_receipt_document_id.sql` - **Suggested Fix**: Specify naming convention or use actual timestamp ### Issue Acceptance Criteria Coverage: 10/10 covered *Verdict*: PASS_WITH_CONCERNS | *Next*: Address concerns, then TW plan-scrub
Author
Owner

Plan Addendum: Addressing QR plan-completeness Concerns

Phase: Planning | Agent: Planner | Status: IN_PROGRESS


1. Policy Defaults (Error Handling)

Scenario Behavior
OCR extraction fails Show error toast ("Could not read receipt. Try again with a clearer image."), reset to capture state, allow retry
OCR extraction timeout 15s timeout on proxy endpoint, show timeout toast with retry option
Document upload fails Show error toast, preserve captured image in state for retry without re-capture
Gemini API unavailable OCR endpoint returns 503 with { error: "OCR_UNAVAILABLE" }, frontend shows retry toast
Network error Frontend catches, shows "Connection error" toast, allows retry
Tier gate blocked Non-Pro: lock icon on button, onClick opens upgrade dialog (existing pattern)
File too large (>10MB) Frontend validates before upload, shows "File too large" error inline
Unsupported file type Frontend validates, shows "Unsupported format" error; backend also validates

2. M3 File Manifest Addition

Add to Milestone 3 files:

File Action Description
frontend/src/features/maintenance/utils/category-mapper.ts NEW Keyword-based service description to category/subtype mapping

Category mapper exports: mapServiceToCategory(serviceName: string): { category: MaintenanceCategory, subtypes: string[] } with keyword lookup table.

3. M4 Receipt Display Mobile/Desktop Specs

Mobile (< 768px):

  • Thumbnail: 64x64px, rounded corners
  • "View Receipt" button: full-width, below thumbnail, 44px height
  • Stack layout: thumbnail on top, button below

Desktop (>= 768px):

  • Thumbnail: 80x80px, rounded corners
  • "View Receipt" button: inline next to thumbnail, standard button size
  • Row layout: thumbnail left, metadata center, button right

4. Testing Strategy

Milestone Testing Approach
M1 (OCR) Unit tests for MaintenanceReceiptExtractor with sample receipt text, cross-validation edge cases (mismatched dates, invalid amounts), Gemini response parsing
M2 (Backend) Integration tests for proxy endpoint (auth, tier gate, forwarding), migration up/down, repository CRUD with receiptDocumentId, FK constraint behavior
M3 (Frontend) Component tests for MaintenanceReceiptReviewModal (render, edit, confidence display), hook state machine transitions
M4 (Frontend) Component tests for form integration (scan button render, tier gate, auto-populate via setValue), receipt display render

5. Migration Filename Convention

Use timestamp format matching existing migrations: {YYYYMMDDHHMMSS}_add_receipt_document_id.sql

Example: 20260213000000_add_receipt_document_id.sql


Verdict: CONCERNS_ADDRESSED | Next: TW plan-scrub

## Plan Addendum: Addressing QR plan-completeness Concerns **Phase**: Planning | **Agent**: Planner | **Status**: IN_PROGRESS --- ### 1. Policy Defaults (Error Handling) | Scenario | Behavior | |----------|----------| | OCR extraction fails | Show error toast ("Could not read receipt. Try again with a clearer image."), reset to capture state, allow retry | | OCR extraction timeout | 15s timeout on proxy endpoint, show timeout toast with retry option | | Document upload fails | Show error toast, preserve captured image in state for retry without re-capture | | Gemini API unavailable | OCR endpoint returns 503 with `{ error: "OCR_UNAVAILABLE" }`, frontend shows retry toast | | Network error | Frontend catches, shows "Connection error" toast, allows retry | | Tier gate blocked | Non-Pro: lock icon on button, onClick opens upgrade dialog (existing pattern) | | File too large (>10MB) | Frontend validates before upload, shows "File too large" error inline | | Unsupported file type | Frontend validates, shows "Unsupported format" error; backend also validates | ### 2. M3 File Manifest Addition Add to Milestone 3 files: | File | Action | Description | |------|--------|-------------| | `frontend/src/features/maintenance/utils/category-mapper.ts` | NEW | Keyword-based service description to category/subtype mapping | Category mapper exports: `mapServiceToCategory(serviceName: string): { category: MaintenanceCategory, subtypes: string[] }` with keyword lookup table. ### 3. M4 Receipt Display Mobile/Desktop Specs **Mobile (< 768px)**: - Thumbnail: 64x64px, rounded corners - "View Receipt" button: full-width, below thumbnail, 44px height - Stack layout: thumbnail on top, button below **Desktop (>= 768px)**: - Thumbnail: 80x80px, rounded corners - "View Receipt" button: inline next to thumbnail, standard button size - Row layout: thumbnail left, metadata center, button right ### 4. Testing Strategy | Milestone | Testing Approach | |-----------|-----------------| | M1 (OCR) | Unit tests for `MaintenanceReceiptExtractor` with sample receipt text, cross-validation edge cases (mismatched dates, invalid amounts), Gemini response parsing | | M2 (Backend) | Integration tests for proxy endpoint (auth, tier gate, forwarding), migration up/down, repository CRUD with receiptDocumentId, FK constraint behavior | | M3 (Frontend) | Component tests for `MaintenanceReceiptReviewModal` (render, edit, confidence display), hook state machine transitions | | M4 (Frontend) | Component tests for form integration (scan button render, tier gate, auto-populate via setValue), receipt display render | ### 5. Migration Filename Convention Use timestamp format matching existing migrations: `{YYYYMMDDHHMMSS}_add_receipt_document_id.sql` Example: `20260213000000_add_receipt_document_id.sql` --- *Verdict*: CONCERNS_ADDRESSED | *Next*: TW plan-scrub
Author
Owner

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

## 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
Author
Owner

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

## 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
Author
Owner

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

  • Location: M1 - MaintenanceReceiptExtractor
  • Issue: Plan does not address Gemini 429/rate limiting scenarios
  • Suggested Fix: Handle Gemini 429 with exponential backoff (max 3 retries), return OCR_RATE_LIMITED error code

[RULE 0] [HIGH]: SQL Injection Risk via receiptDocumentId

  • Location: M2 - Repository queries
  • Issue: Plan doesn't explicitly state parameterized query usage for new FK
  • Suggested Fix: Explicitly require parameterized queries ($N placeholders) for all receipt_document_id queries

[RULE 1] [HIGH]: Mobile Interaction for Review Modal Secondary Fields

  • Location: M3 - MaintenanceReceiptReviewModal
  • Issue: "Secondary fields (collapsible)" doesn't specify mobile interaction pattern
  • Suggested Fix: Mobile uses MUI Accordion, desktop uses inline collapsible. 44px touch target for expand/collapse.

[RULE 0] [SHOULD_FIX]: File Upload MIME Type Verification

  • Location: M1 - FastAPI endpoint
  • Note: Existing ReceiptExtractor._detect_mime_type() already uses python-magic. New extractor inherits this. Acknowledged.

[RULE 1] [SHOULD_FIX]: receiptDocumentId Zod UUID Validation

  • Location: M2 - CreateMaintenanceRecordSchema
  • Suggested Fix: Validate with .uuid() in Zod. Service layer validates document exists and belongs to user.

[RULE 2] [SHOULD_FIX]: Confidence Constants Duplication

  • Location: M1 - Cross-validation
  • Suggested Fix: Extract to shared module app/patterns/confidence.py (CONFIDENCE_BOOST = 1.1, CONFIDENCE_PENALTY = 0.8)

[RULE 2] [SUGGESTION]: Category Mapper Maintainability

  • Hardcoded keyword mapping for MVP is acceptable. Document as future enhancement.

Considered But Not Flagged

  • Hook state machine: no race conditions, blob URL cleanup follows existing pattern
  • Migration rollback: safe (FK column drop)
  • Repository mapRow(): follows existing snake_case -> camelCase convention
  • OCR result reuse: correct, no duplication
  • Frontend naming: all camelCase, consistent

Verdict: PASS_WITH_CONCERNS | Next: Address HIGH findings, then QR plan-docs

## 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** - **Location**: M1 - MaintenanceReceiptExtractor - **Issue**: Plan does not address Gemini 429/rate limiting scenarios - **Suggested Fix**: Handle Gemini 429 with exponential backoff (max 3 retries), return `OCR_RATE_LIMITED` error code **[RULE 0] [HIGH]: SQL Injection Risk via receiptDocumentId** - **Location**: M2 - Repository queries - **Issue**: Plan doesn't explicitly state parameterized query usage for new FK - **Suggested Fix**: Explicitly require parameterized queries ($N placeholders) for all receipt_document_id queries **[RULE 1] [HIGH]: Mobile Interaction for Review Modal Secondary Fields** - **Location**: M3 - MaintenanceReceiptReviewModal - **Issue**: "Secondary fields (collapsible)" doesn't specify mobile interaction pattern - **Suggested Fix**: Mobile uses MUI Accordion, desktop uses inline collapsible. 44px touch target for expand/collapse. **[RULE 0] [SHOULD_FIX]: File Upload MIME Type Verification** - **Location**: M1 - FastAPI endpoint - **Note**: Existing `ReceiptExtractor._detect_mime_type()` already uses python-magic. New extractor inherits this. Acknowledged. **[RULE 1] [SHOULD_FIX]: receiptDocumentId Zod UUID Validation** - **Location**: M2 - CreateMaintenanceRecordSchema - **Suggested Fix**: Validate with `.uuid()` in Zod. Service layer validates document exists and belongs to user. **[RULE 2] [SHOULD_FIX]: Confidence Constants Duplication** - **Location**: M1 - Cross-validation - **Suggested Fix**: Extract to shared module `app/patterns/confidence.py` (CONFIDENCE_BOOST = 1.1, CONFIDENCE_PENALTY = 0.8) **[RULE 2] [SUGGESTION]: Category Mapper Maintainability** - Hardcoded keyword mapping for MVP is acceptable. Document as future enhancement. ### Considered But Not Flagged - Hook state machine: no race conditions, blob URL cleanup follows existing pattern - Migration rollback: safe (FK column drop) - Repository mapRow(): follows existing snake_case -> camelCase convention - OCR result reuse: correct, no duplication - Frontend naming: all camelCase, consistent *Verdict*: PASS_WITH_CONCERNS | *Next*: Address HIGH findings, then QR plan-docs
Author
Owner

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)

## 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)
egullickson added
status
review
and removed
status
in-progress
labels 2026-02-13 16:34:56 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#16