import { NextResponse } from 'next/server'; import { isAdminAuthenticated } from '@/lib/adminAuth'; import { prisma } from '@/lib/prisma'; import * as Tesseract from 'tesseract.js'; type ExtractedItem = { description: string; quantity: number; productId?: string; }; async function extractTextFromPDF(buffer: Buffer): Promise { try { // PDF parsing disabled - pdf-parse module compatibility issue // This feature can be re-enabled by fixing the pdf-parse import return ''; } catch (err) { throw new Error('Failed to extract text from PDF'); } } async function extractTextFromImage(buffer: Buffer): Promise { try { const result = await Tesseract.recognize(buffer, 'eng'); return result.data.text; } catch (err) { throw new Error('Failed to OCR image'); } } function parseInvoiceText(text: string, products: any[]): ExtractedItem[] { const items: ExtractedItem[] = []; const lines = text.split('\n'); // Build product search index (lowercase for matching) const productIndex = new Map(products.map((p) => [p.name.toLowerCase(), p.id])); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.length < 5) continue; // Look for patterns like: quantity x product or product x quantity // Examples: "10 x T-Shirt", "T-Shirt - 10", "10x hoodies" const matches = trimmed.match(/(\d+)\s*x?\s*(.+?)(?:\s*-\s*£?[\d.]+)?$/i); if (!matches) continue; const quantity = parseInt(matches[1], 10); let description = matches[2].trim(); // Clean up description (remove common noise) description = description.replace(/\s*-\s*\d+\s*$/, ''); // Remove trailing numbers description = description.replace(/£[\d.]+/, '').trim(); // Remove prices if (quantity > 0 && description.length > 2) { // Try to match to a product const lowerDesc = description.toLowerCase(); let productId: string | undefined; for (const [productName, pId] of productIndex) { if (lowerDesc.includes(productName)) { productId = pId; break; } } items.push({ description, quantity, productId, }); } } return items; } export async function POST(req: Request) { const isAdmin = await isAdminAuthenticated(); if (!isAdmin) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } try { const formData = await req.formData(); const file = formData.get('invoice') as File; if (!file) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }); } if (file.size > 8 * 1024 * 1024) { return NextResponse.json({ error: 'File too large (max 8MB)' }, { status: 400 }); } const buffer = Buffer.from(await file.arrayBuffer()); let text = ''; if (file.type === 'application/pdf') { text = await extractTextFromPDF(buffer); } else if (file.type.startsWith('image/')) { text = await extractTextFromImage(buffer); } else { return NextResponse.json( { error: 'Unsupported file type. Use PDF or image.' }, { status: 400 } ); } // Get all products for matching const products = await prisma.product.findMany({ select: { id: true, name: true }, }); const extractedItems = parseInvoiceText(text, products); return NextResponse.json({ items: extractedItems, rawText: text.substring(0, 500), // First 500 chars for debugging }); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return NextResponse.json({ error: message }, { status: 500 }); } }