feat: add backend OCR manual proxy endpoint (refs #135)

Add POST /api/ocr/extract/manual endpoint that proxies to the Python
OCR service's manual extraction pipeline. Includes Pro tier gating via
document.scanMaintenanceSchedule, PDF-only validation, 200MB file size
limit, and async 202 job response for polling via existing job status
endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-11 10:37:18 -06:00
parent 57ed04d955
commit a281cea9c5
6 changed files with 489 additions and 1 deletions

View File

@@ -2,7 +2,7 @@
* @ai-summary HTTP client for OCR service communication
*/
import { logger } from '../../../core/logging/logger';
import type { JobResponse, OcrResponse, ReceiptExtractionResponse, VinExtractionResponse } from '../domain/ocr.types';
import type { JobResponse, ManualJobResponse, OcrResponse, ReceiptExtractionResponse, VinExtractionResponse } from '../domain/ocr.types';
/** OCR service configuration */
const OCR_SERVICE_URL = process.env.OCR_SERVICE_URL || 'http://mvp-ocr:8000';
@@ -265,6 +265,61 @@ export class OcrClient {
return (await response.json()) as JobResponse;
}
/**
* Submit an async manual extraction job for PDF owner's manuals.
*
* @param fileBuffer - PDF file buffer
* @param contentType - MIME type of the file (must be application/pdf)
* @param vehicleId - Optional vehicle ID for context
* @returns Manual job submission response
*/
async submitManualJob(
fileBuffer: Buffer,
contentType: string,
vehicleId?: string
): Promise<ManualJobResponse> {
const formData = this.buildFormData(fileBuffer, contentType);
if (vehicleId) {
formData.append('vehicle_id', vehicleId);
}
const url = `${this.baseUrl}/extract/manual`;
logger.info('OCR manual job submit request', {
operation: 'ocr.client.submitManualJob',
url,
contentType,
fileSize: fileBuffer.length,
hasVehicleId: !!vehicleId,
});
const response = await this.fetchWithTimeout(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
logger.error('OCR manual job submit failed', {
operation: 'ocr.client.submitManualJob.error',
status: response.status,
error: errorText,
});
throw new Error(`OCR service error: ${response.status} - ${errorText}`);
}
const result = (await response.json()) as ManualJobResponse;
logger.info('OCR manual job submitted', {
operation: 'ocr.client.submitManualJob.success',
jobId: result.jobId,
status: result.status,
estimatedSeconds: result.estimatedSeconds,
});
return result;
}
/**
* Check if the OCR service is healthy.
*