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
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
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,
|
||
},
|
||
});
|
||
});
|
||
});
|