/** * @ai-summary Hook to orchestrate VIN OCR extraction and VIN decode * @ai-context Handles camera capture -> OCR extraction -> VIN decode flow */ import { useState, useCallback } from 'react'; import { apiClient } from '../../../core/api/client'; import { vehiclesApi } from '../api/vehicles.api'; import { DecodedVehicleData } from '../types/vehicles.types'; /** OCR extraction result for VIN */ export interface VinOcrResult { vin: string; confidence: number; rawText: string; } /** Combined OCR + decode result */ export interface VinCaptureResult { ocrResult: VinOcrResult; decodedVehicle: DecodedVehicleData | null; decodeError: string | null; } /** Hook state */ export interface UseVinOcrState { isCapturing: boolean; isProcessing: boolean; processingStep: 'idle' | 'extracting' | 'decoding'; result: VinCaptureResult | null; error: string | null; } /** Hook return type */ export interface UseVinOcrReturn extends UseVinOcrState { startCapture: () => void; cancelCapture: () => void; processImage: (file: File, croppedFile?: File) => Promise; acceptResult: () => VinCaptureResult | null; reset: () => void; } /** * Extract VIN from image using VIN-specific OCR endpoint */ async function extractVinFromImage(file: File): Promise { const formData = new FormData(); formData.append('file', file); const response = await apiClient.post('/ocr/extract/vin', formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 120000, // 120 seconds for OCR processing }); const data = response.data; if (!data.success) { throw new Error(data.error || 'VIN extraction failed'); } if (!data.vin) { throw new Error('No VIN found in image. Please ensure the VIN is clearly visible.'); } return { vin: data.vin.toUpperCase().replace(/[^A-HJ-NPR-Z0-9]/g, ''), confidence: data.confidence, rawText: data.vin, }; } /** * Hook to orchestrate VIN photo capture, OCR, and decode */ export function useVinOcr(): UseVinOcrReturn { const [isCapturing, setIsCapturing] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [processingStep, setProcessingStep] = useState<'idle' | 'extracting' | 'decoding'>('idle'); const [result, setResult] = useState(null); const [error, setError] = useState(null); const startCapture = useCallback(() => { setIsCapturing(true); setError(null); setResult(null); }, []); const cancelCapture = useCallback(() => { setIsCapturing(false); setError(null); }, []); const processImage = useCallback(async (file: File, croppedFile?: File) => { setIsCapturing(false); setIsProcessing(true); setError(null); setResult(null); try { // Step 1: Extract VIN from image setProcessingStep('extracting'); const imageToProcess = croppedFile || file; const ocrResult = await extractVinFromImage(imageToProcess); // Validate VIN format if (ocrResult.vin.length !== 17) { throw new Error( `Extracted VIN "${ocrResult.vin}" is ${ocrResult.vin.length} characters. VIN must be exactly 17 characters.` ); } // Step 2: Decode VIN setProcessingStep('decoding'); let decodedVehicle: DecodedVehicleData | null = null; let decodeError: string | null = null; try { decodedVehicle = await vehiclesApi.decodeVin(ocrResult.vin); } catch (err: any) { // VIN decode failure is not fatal - we still have the VIN if (err.response?.data?.error === 'TIER_REQUIRED') { decodeError = 'VIN decode requires Pro or Enterprise subscription'; } else if (err.response?.data?.error === 'INVALID_VIN') { decodeError = 'VIN format is not recognized'; } else { decodeError = 'Unable to decode vehicle information'; } console.warn('VIN decode failed:', err); } setResult({ ocrResult, decodedVehicle, decodeError, }); } catch (err: any) { console.error('VIN OCR processing failed:', err); const message = err.response?.data?.message || err.message || 'Failed to process image'; setError(message); } finally { setIsProcessing(false); setProcessingStep('idle'); } }, []); const acceptResult = useCallback(() => { const currentResult = result; setResult(null); return currentResult; }, [result]); const reset = useCallback(() => { setIsCapturing(false); setIsProcessing(false); setProcessingStep('idle'); setResult(null); setError(null); }, []); return { isCapturing, isProcessing, processingStep, result, error, startCapture, cancelCapture, processImage, acceptResult, reset, }; }