# OCR Feature

Backend proxy for the Python OCR microservice. Handles authentication, tier gating, file validation, and request forwarding for three extraction types: VIN decoding, fuel receipt scanning, and maintenance manual extraction.

## API Endpoints

| Method | Endpoint | Description | Auth | Tier | Max Size |
|--------|----------|-------------|------|------|----------|
| POST | `/api/ocr/extract` | Synchronous general OCR extraction | Required | - | 10MB |
| POST | `/api/ocr/extract/vin` | VIN-specific extraction | Required | - | 10MB |
| POST | `/api/ocr/extract/receipt` | Fuel receipt extraction | Required | Pro | 10MB |
| POST | `/api/ocr/extract/manual` | Async maintenance manual extraction | Required | Pro | 200MB |
| POST | `/api/ocr/jobs` | Submit async OCR job | Required | - | 200MB |
| GET | `/api/ocr/jobs/:jobId` | Poll async job status | Required | - | - |

## Architecture

```
Frontend
   |
   v
Backend Proxy (this feature)
   |
   +-- ocr.routes.ts --------> Route registration (auth + tier preHandlers)
   |
   +-- ocr.controller.ts ----> Request handlers (file validation, size checks)
   |
   +-- ocr.service.ts -------> Business logic (content type validation, delegation)
   |
   +-- ocr-client.ts --------> HTTP client to mvp-ocr:8000
                                   |
                                   v
                            Python OCR Service
```

## Receipt OCR Flow

```
Mobile Camera / File Upload
    |
    v
POST /api/ocr/extract/receipt (multipart/form-data)
    |
    v
OcrController.extractReceipt()
    - Validates file size (<= 10MB)
    - Validates content type (JPEG, PNG, HEIC)
    |
    v
OcrService.extractReceipt()
    |
    v
OcrClient.extractReceipt() --> HTTP POST --> Python /extract/receipt
    |                                             |
    v                                             v
ReceiptExtractionResponse                  ReceiptExtractor + HybridEngine
    |                                      (Vision API / PaddleOCR fallback)
    v
Frontend receives extractedFields:
    merchantName, transactionDate, totalAmount,
    fuelQuantity, pricePerUnit, fuelGrade
```

After receipt extraction, the frontend calls `POST /api/stations/match` with the `merchantName` to auto-match a gas station via Google Places API. The station match is a separate request handled by the stations feature.

## Manual Extraction Flow

```
PDF Upload + "Scan for Maintenance Schedule"
    |
    v
POST /api/ocr/extract/manual (multipart/form-data)
    - Requires Pro tier (document.scanMaintenanceSchedule)
    - Validates file size (<= 200MB)
    - Validates content type (application/pdf)
    - Validates PDF magic bytes (%PDF header)
    |
    v
OcrService.submitManualJob()
    |
    v
OcrClient.submitManualJob() --> HTTP POST --> Python /extract/manual
    |                                              |
    v                                              v
{ jobId, status: 'pending' }               GeminiEngine (Vertex AI)
                                           Gemini 2.5 Flash
    Frontend polls:                        (structured JSON output)
    GET /api/ocr/jobs/:jobId                     |
    (progress: 10% -> 50% -> 95% -> 100%)       v
    |                                      ManualExtractionResult
    v                                      { vehicleInfo, maintenanceSchedules[] }
ManualJobResponse with result
    |
    v
Frontend displays MaintenanceScheduleReviewScreen
    - User selects/edits items
    - Batch creates maintenance schedules
```

Jobs expire after 2 hours (Redis TTL). Expired job polling returns HTTP 410 Gone.

## Supported File Types

### Sync Endpoints (extract, extractVin, extractReceipt)
- HEIC (converted server-side)
- JPEG
- PNG

### Async Endpoints (extractManual)
- PDF (validated via magic bytes)

## Response Types

### ReceiptExtractionResponse
```typescript
{
  success: boolean;
  receiptType: string;
  extractedFields: {
    merchantName: { value: string; confidence: number };
    transactionDate: { value: string; confidence: number };
    totalAmount: { value: string; confidence: number };
    fuelQuantity: { value: string; confidence: number };
    pricePerUnit: { value: string; confidence: number };
    fuelGrade: { value: string; confidence: number };
  };
  rawText: string;
  processingTimeMs: number;
}
```

### ManualJobResponse
```typescript
{
  jobId: string;
  status: 'pending' | 'processing' | 'completed' | 'failed';
  progress?: { percent: number; message: string };
  estimatedSeconds?: number;
  result?: ManualExtractionResult;
  error?: string;
}
```

### ManualExtractionResult
```typescript
{
  success: boolean;
  vehicleInfo?: { make: string; model: string; year: number };
  maintenanceSchedules: Array<{
    serviceName: string;
    intervalMiles: number | null;
    intervalMonths: number | null;
    details: string;
    confidence: number;
    subtypes: string[];
  }>;
  rawTables: any[];
  processingTimeMs: number;
  totalPages: number;
  pagesProcessed: number;
}
```

## Error Handling

The backend proxy translates Python service error codes:

| Python Status | Backend Status | Meaning |
|---------------|----------------|---------|
| 413 | 413 | File too large |
| 415 | 415 | Unsupported media type |
| 422 | 422 | Extraction failed |
| 410 | 410 | Job expired (TTL) |
| Other | 500 | Internal server error |

## Tier Gating

Manual extraction requires Pro tier. The tier guard middleware (`requireTier` plugin) validates the user's subscription tier before processing. Free-tier users receive HTTP 403 with `TIER_REQUIRED` error code and an upgrade prompt.

VIN extraction is available to all tiers. Receipt extraction requires Pro tier (`fuelLog.receiptScan`).
