Files
pn-new-crm/tests/e2e/realapi/receipt-ocr.spec.ts
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

133 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
});
});
});