import 'dotenv/config'; import { test, expect } from '@playwright/test'; import { promises as fs } from 'fs'; import { login, apiHeaders, getPortId } from '../smoke/helpers'; /** * Real-API receipt OCR coverage. Two-step: * * 1. Admin save + test-connection round-trip: writes a real OpenAI key * to the global OCR config, calls /admin/ocr-settings/test (which * sends a 1×1 pixel PNG — essentially free in tokens), and asserts * the provider responds 2xx. Validates the auth + key-storage path. * * 2. Real receipt parse: when REALAPI_RECEIPT_FIXTURE is set to an * image on disk, POSTs it to /api/v1/expenses/scan-receipt and * asserts the parsed payload looks plausible (numeric amount >= 0, * non-empty parsed.confidence). * * Both tests skip when OPENAI_API_KEY isn't set so the suite remains * CI-safe by default. */ const OPENAI_API_KEY = process.env.OPENAI_API_KEY; const RECEIPT_FIXTURE = process.env.REALAPI_RECEIPT_FIXTURE; test.describe('Receipt OCR — real provider', () => { test.skip(!OPENAI_API_KEY, 'OPENAI_API_KEY not configured'); test('admin can save an OpenAI key and the test endpoint passes', async ({ page }) => { await login(page, 'super_admin'); const headers = await apiHeaders(page); // Save a global OCR config with the real key. Super-admin only. const saveRes = await page.request.put('/api/v1/admin/ocr-settings', { headers, data: { scope: 'global', provider: 'openai', model: 'gpt-4o-mini', apiKey: OPENAI_API_KEY, }, }); expect(saveRes.ok()).toBeTruthy(); const testRes = await page.request.post('/api/v1/admin/ocr-settings/test', { headers, data: { provider: 'openai', model: 'gpt-4o-mini', apiKey: OPENAI_API_KEY, }, }); expect(testRes.ok()).toBeTruthy(); const body = (await testRes.json()) as { ok: boolean; reason?: string }; expect(body.ok).toBe(true); // Cleanup: clear the global key so subsequent test runs don't accidentally // bill the same OpenAI account if someone forgets to unset it. const cleanupRes = await page.request.put('/api/v1/admin/ocr-settings', { headers, data: { scope: 'global', provider: 'openai', model: 'gpt-4o-mini', clearApiKey: true, }, }); expect(cleanupRes.ok()).toBeTruthy(); }); test('scan-receipt endpoint returns a parsed payload for a real image', async ({ page }) => { test.skip(!RECEIPT_FIXTURE, 'REALAPI_RECEIPT_FIXTURE not set'); await login(page, 'super_admin'); const portId = await getPortId(page); // Configure the per-port OCR with the test key for the duration of this run. await page.request.put('/api/v1/admin/ocr-settings', { headers: { 'Content-Type': 'application/json', 'X-Port-Id': portId }, data: { scope: 'port', provider: 'openai', model: 'gpt-4o-mini', apiKey: OPENAI_API_KEY, }, }); const buffer = await fs.readFile(RECEIPT_FIXTURE!); const res = await page.request.post('/api/v1/expenses/scan-receipt', { headers: { 'X-Port-Id': portId }, multipart: { file: { name: 'receipt.jpg', mimeType: 'image/jpeg', buffer, }, }, }); expect(res.ok()).toBeTruthy(); const body = (await res.json()) as { data: { parsed: { amount: number | null; confidence: number; establishment: string | null; date: string | null; }; source: 'ai' | 'manual'; }; }; expect(body.data.source).toBe('ai'); // Confidence must be a valid number 0..1 — provider should always emit it. expect(body.data.parsed.confidence).toBeGreaterThanOrEqual(0); expect(body.data.parsed.confidence).toBeLessThanOrEqual(1); // Amount, if present, should be non-negative. if (body.data.parsed.amount !== null) { expect(body.data.parsed.amount).toBeGreaterThanOrEqual(0); } // Cleanup await page.request.put('/api/v1/admin/ocr-settings', { headers: { 'Content-Type': 'application/json', 'X-Port-Id': portId }, data: { scope: 'port', provider: 'openai', model: 'gpt-4o-mini', clearApiKey: true, }, }); }); });