feat: add backend migration and API for maintenance receipt linking (refs #151)

Add receipt_document_id FK on maintenance_records, update types/repo/service
to support receipt linking on create and return document metadata on GET.
Add OCR proxy endpoint POST /api/ocr/extract/maintenance-receipt with
tier gating (maintenance.receiptScan) through full chain: routes -> controller
-> service -> client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-12 21:24:24 -06:00
parent 90401dc1ba
commit 88d23d2745
10 changed files with 285 additions and 7 deletions

View File

@@ -177,6 +177,57 @@ export class OcrClient {
return result;
}
/**
* Extract data from a maintenance receipt image using maintenance-specific OCR.
*
* @param fileBuffer - Image file buffer
* @param contentType - MIME type of the file
* @returns Receipt extraction result (receiptType: "maintenance")
*/
async extractMaintenanceReceipt(
fileBuffer: Buffer,
contentType: string
): Promise<ReceiptExtractionResponse> {
const formData = this.buildFormData(fileBuffer, contentType);
const url = `${this.baseUrl}/extract/maintenance-receipt`;
logger.info('OCR maintenance receipt extract request', {
operation: 'ocr.client.extractMaintenanceReceipt',
url,
contentType,
fileSize: fileBuffer.length,
});
const response = await this.fetchWithTimeout(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
logger.error('OCR maintenance receipt extract failed', {
operation: 'ocr.client.extractMaintenanceReceipt.error',
status: response.status,
error: errorText,
});
const err: any = new Error(`OCR service error: ${response.status} - ${errorText}`);
err.statusCode = response.status;
throw err;
}
const result = (await response.json()) as ReceiptExtractionResponse;
logger.info('OCR maintenance receipt extract completed', {
operation: 'ocr.client.extractMaintenanceReceipt.success',
success: result.success,
receiptType: result.receiptType,
fieldCount: Object.keys(result.extractedFields).length,
processingTimeMs: result.processingTimeMs,
});
return result;
}
/**
* Submit an async OCR job for large files.
*