Add New Product

All Products

Supplier Description Cost VAT Actions

Create Invoice

Product Qty
Total: £0.00
You still need your config.js with your Firebase settings (same as before).2) Cloudflare Worker file (AI + Gemini)Create this file in your Workers project:src/index.jsCopy EVERYTHING below into src/index.js:export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (url.pathname !== '/ai/parse-invoice') { return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } }); } if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { 'Content-Type': 'application/json' } }); } try { const apiKey = env.GEMINI_API_KEY; if (!apiKey) { return new Response( JSON.stringify({ error: 'Gemini API key not configured' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } const body = await request.json(); const ocrText = body.ocrText || ''; if (!ocrText.trim()) { return new Response( JSON.stringify({ error: 'Missing ocrText' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } const payload = { contents: [ { parts: [ { text: `You are helping parse a UK food supplier invoice or product label into structured fields. OCR text: """${ocrText}""" Return ONLY valid JSON with this exact shape and no extra keys: { "supplier": "string - supplier name", "description": "string - main product description", "cost": 0.00, "vat": true } Rules: - cost is the most relevant single unit price in GBP (numbers only, no currency sign). - vat is true if the item appears VATable (e.g. VAT shown, standard-rated product), false otherwise. - If unsure, make your best guess but never return null, always fill something reasonable.` } ] } ], generationConfig: { temperature: 0.2, response_mime_type: "application/json" } }; const geminiResp = await fetch( 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }, body: JSON.stringify(payload) } ); if (!geminiResp.ok) { const errText = await geminiResp.text(); console.error('Gemini error response:', errText); return new Response( JSON.stringify({ error: 'Gemini API error', details: errText }), { status: geminiResp.status, headers: { 'Content-Type': 'application/json' } } ); } const data = await geminiResp.json(); let jsonText = ''; try { jsonText = data.candidates?.[0]?.content?.parts?.[0]?.text || '{}'; } catch (e) { console.error('Error reading Gemini response:', e); jsonText = '{}'; } let parsed; try { parsed = JSON.parse(jsonText); } catch (e) { console.error('Gemini returned non-JSON, raw:', jsonText); parsed = {}; } const result = { supplier: parsed.supplier || '', description: parsed.description || '', cost: typeof parsed.cost === 'number' ? parsed.cost : 0, vat: typeof parsed.vat === 'boolean' ? parsed.vat : false }; return new Response(JSON.stringify(result), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (err) { console.error('Worker error:', err); return new Response( JSON.stringify({ error: 'Server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } } };