feat: Backend OCR receipt proxy endpoint (#129) #139

Closed
opened 2026-02-11 03:48:55 +00:00 by egullickson · 1 comment
Owner

Relates to #129

Milestone 1: Backend OCR Receipt Proxy Endpoint

Files

  • backend/src/features/ocr/domain/ocr.types.ts
  • backend/src/features/ocr/external/ocr-client.ts
  • backend/src/features/ocr/domain/ocr.service.ts
  • backend/src/features/ocr/api/ocr.controller.ts
  • backend/src/features/ocr/api/ocr.routes.ts

Requirements

  • Add ReceiptExtractionResponse type with receiptType, extractedFields, rawText, processingTimeMs
  • Add OcrClient.extractReceipt() method that POSTs to Python /extract/receipt with optional receipt_type=fuel form field
  • Add ocrService.extractReceipt() method with file validation (10MB max, image types only)
  • Add OcrController.extractReceipt() handler with file upload, validation, error mapping
  • OcrController.extractReceipt() translates Python error codes: 413->413, 415->415, 422->422, else 500 (matches pattern from OcrController.extract())
  • Add POST /api/ocr/extract/receipt route with preHandler: [requireAuth, requireTier('fuelLog.receiptScan')]
  • Backend returns 403 TIER_REQUIRED for free-tier users (backend enforcement, not frontend-only)

Acceptance Criteria

  • POST /api/ocr/extract/receipt with valid image returns ReceiptExtractionResponse with extractedFields
  • POST with no file returns 400
  • POST with unsupported type returns 415
  • POST with file > 10MB returns 413
  • POST by free-tier user returns 403 TIER_REQUIRED
  • Response includes merchantName, transactionDate, totalAmount, fuelQuantity, pricePerUnit, fuelGrade fields with confidence scores
  • Python 422 "Failed to extract" forwarded as 422 to frontend

Tests

  • Test files: backend/src/features/ocr/tests/unit/ocr-receipt.test.ts (NEW)
  • Test type: unit (mock OcrClient HTTP calls)
  • Scenarios:
    • Normal: Valid image returns receipt extraction response
    • Edge: Missing optional fields (some fields not detected)
    • Error: No file returns 400, unsupported type returns 415, oversized file returns 413
    • Error: Python 422 forwarded correctly
    • Error: Free-tier user returns 403 TIER_REQUIRED
Relates to #129 ## Milestone 1: Backend OCR Receipt Proxy Endpoint ### Files - `backend/src/features/ocr/domain/ocr.types.ts` - `backend/src/features/ocr/external/ocr-client.ts` - `backend/src/features/ocr/domain/ocr.service.ts` - `backend/src/features/ocr/api/ocr.controller.ts` - `backend/src/features/ocr/api/ocr.routes.ts` ### Requirements - Add `ReceiptExtractionResponse` type with `receiptType`, `extractedFields`, `rawText`, `processingTimeMs` - Add `OcrClient.extractReceipt()` method that POSTs to Python `/extract/receipt` with optional `receipt_type=fuel` form field - Add `ocrService.extractReceipt()` method with file validation (10MB max, image types only) - Add `OcrController.extractReceipt()` handler with file upload, validation, error mapping - `OcrController.extractReceipt()` translates Python error codes: 413->413, 415->415, 422->422, else 500 (matches pattern from `OcrController.extract()`) - Add `POST /api/ocr/extract/receipt` route with `preHandler: [requireAuth, requireTier('fuelLog.receiptScan')]` - Backend returns 403 TIER_REQUIRED for free-tier users (backend enforcement, not frontend-only) ### Acceptance Criteria - POST /api/ocr/extract/receipt with valid image returns ReceiptExtractionResponse with extractedFields - POST with no file returns 400 - POST with unsupported type returns 415 - POST with file > 10MB returns 413 - POST by free-tier user returns 403 TIER_REQUIRED - Response includes merchantName, transactionDate, totalAmount, fuelQuantity, pricePerUnit, fuelGrade fields with confidence scores - Python 422 "Failed to extract" forwarded as 422 to frontend ### Tests - **Test files**: `backend/src/features/ocr/tests/unit/ocr-receipt.test.ts` (NEW) - **Test type**: unit (mock OcrClient HTTP calls) - **Scenarios**: - Normal: Valid image returns receipt extraction response - Edge: Missing optional fields (some fields not detected) - Error: No file returns 400, unsupported type returns 415, oversized file returns 413 - Error: Python 422 forwarded correctly - Error: Free-tier user returns 403 TIER_REQUIRED
egullickson added the
status
backlog
type
feature
labels 2026-02-11 03:51:12 +00:00
egullickson added
status
in-progress
and removed
status
backlog
labels 2026-02-11 17:17:14 +00:00
Author
Owner

Milestone: Backend OCR Receipt Proxy Endpoint

Phase: Execution | Agent: Developer | Status: PASS

Changes

  1. ocr.routes.ts: Added requireTier('fuelLog.receiptScan') to POST /api/ocr/extract/receipt route. Free-tier users now receive 403 TIER_REQUIRED before the handler executes.

  2. ocr-client.ts: Updated extractReceipt() error handling to attach statusCode from the Python OCR service HTTP response to thrown errors, enabling controller-level error code forwarding.

  3. ocr.controller.ts: Added 422 error handling in extractReceipt catch block. Python 422 "Failed to extract" errors are now forwarded as 422 to the frontend (matching the pattern from extract()).

  4. ocr-receipt.test.ts: Added 2 new tests:

    • Python 422 forwarded with statusCode for controller forwarding
    • Route tier guard configuration verification

Test Results

  • 11/11 tests pass in ocr-receipt.test.ts
  • Type-check: PASS
  • Lint: PASS (no new warnings)

Acceptance Criteria

  • POST /api/ocr/extract/receipt with valid image returns ReceiptExtractionResponse with extractedFields
  • POST with no file returns 400
  • POST with unsupported type returns 415
  • POST with file > 10MB returns 413
  • POST by free-tier user returns 403 TIER_REQUIRED
  • Response includes merchantName, transactionDate, totalAmount, fuelQuantity, pricePerUnit, fuelGrade fields with confidence scores
  • Python 422 "Failed to extract" forwarded as 422 to frontend

Verdict: PASS | Next: Complete

## Milestone: Backend OCR Receipt Proxy Endpoint **Phase**: Execution | **Agent**: Developer | **Status**: PASS ### Changes 1. **`ocr.routes.ts`**: Added `requireTier('fuelLog.receiptScan')` to `POST /api/ocr/extract/receipt` route. Free-tier users now receive 403 TIER_REQUIRED before the handler executes. 2. **`ocr-client.ts`**: Updated `extractReceipt()` error handling to attach `statusCode` from the Python OCR service HTTP response to thrown errors, enabling controller-level error code forwarding. 3. **`ocr.controller.ts`**: Added 422 error handling in `extractReceipt` catch block. Python 422 "Failed to extract" errors are now forwarded as 422 to the frontend (matching the pattern from `extract()`). 4. **`ocr-receipt.test.ts`**: Added 2 new tests: - Python 422 forwarded with statusCode for controller forwarding - Route tier guard configuration verification ### Test Results - 11/11 tests pass in `ocr-receipt.test.ts` - Type-check: PASS - Lint: PASS (no new warnings) ### Acceptance Criteria - [x] POST /api/ocr/extract/receipt with valid image returns ReceiptExtractionResponse with extractedFields - [x] POST with no file returns 400 - [x] POST with unsupported type returns 415 - [x] POST with file > 10MB returns 413 - [x] POST by free-tier user returns 403 TIER_REQUIRED - [x] Response includes merchantName, transactionDate, totalAmount, fuelQuantity, pricePerUnit, fuelGrade fields with confidence scores - [x] Python 422 "Failed to extract" forwarded as 422 to frontend *Verdict*: PASS | *Next*: Complete
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: egullickson/motovaultpro#139