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

@@ -5,6 +5,8 @@ import { logger } from '../../../core/logging/logger';
import { ocrClient, JobNotFoundError } from '../external/ocr-client';
import type {
JobResponse,
ManualJobResponse,
ManualJobSubmitRequest,
OcrExtractRequest,
OcrJobSubmitRequest,
OcrResponse,
@@ -278,6 +280,66 @@ export class OcrService {
}
}
/**
* Submit an async manual extraction job for PDF owner's manuals.
*
* @param userId - User ID for logging
* @param request - Manual job submission request
* @returns Manual job response with job ID
*/
async submitManualJob(userId: string, request: ManualJobSubmitRequest): Promise<ManualJobResponse> {
// Validate file size for async processing (200MB max)
if (request.fileBuffer.length > MAX_ASYNC_SIZE) {
const err: any = new Error(
`File too large. Max: ${MAX_ASYNC_SIZE / (1024 * 1024)}MB.`
);
err.statusCode = 413;
throw err;
}
// Manual extraction only supports PDF
if (request.contentType !== 'application/pdf') {
const err: any = new Error(
`Unsupported file type: ${request.contentType}. Manual extraction requires PDF files.`
);
err.statusCode = 400;
throw err;
}
logger.info('Manual job submit requested', {
operation: 'ocr.service.submitManualJob',
userId,
contentType: request.contentType,
fileSize: request.fileBuffer.length,
hasVehicleId: !!request.vehicleId,
});
try {
const result = await ocrClient.submitManualJob(
request.fileBuffer,
request.contentType,
request.vehicleId
);
logger.info('Manual job submitted', {
operation: 'ocr.service.submitManualJob.success',
userId,
jobId: result.jobId,
status: result.status,
estimatedSeconds: result.estimatedSeconds,
});
return result;
} catch (error) {
logger.error('Manual job submit failed', {
operation: 'ocr.service.submitManualJob.error',
userId,
error: error instanceof Error ? error.message : 'Unknown error',
});
throw error;
}
}
/**
* Get the status of an async OCR job.
*