feat(crm): client-meeting batch — contact-pill cleanup, assignment toggle, receipt manual mode
CM-4: remove Email/Call/WhatsApp deep-link pills from the client + interest detail headers; relocate GDPR export into the client-header action cluster as a compact icon. Keeps the interest "Log contact" quick action. CM-5: gate the interest assignment feature behind a per-port `assignment_enabled` setting (default OFF for single-rep ports). Hides the AssignedToChip + residential assigned-to row and skips tier-2/3 auto-assign on create; the column + data are preserved and reversible. Tests cover the auto-assign guard. CM-6: add a per-port `manualEntry` receipt mode (skip all parsing → empty form). Threaded through ocr-config.service, the admin OCR form, the scan-receipt route, and the scanner shell (skips Tesseract + the server call). Tests cover the save/resolve round-trip. Verified: tsc clean, lint 0 errors, 1631 vitest pass, prod build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,60 @@ describe('OCR config', () => {
|
||||
expect(resolved.aiEnabled).toBe(false);
|
||||
});
|
||||
|
||||
// CM-6: manual-entry mode (skip all parsing) - mirrors the aiEnabled contract.
|
||||
it('manualEntry defaults to false and round-trips when toggled', async () => {
|
||||
const port = await makePort();
|
||||
await saveOcrConfig(
|
||||
port.id,
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: 'sk-y' },
|
||||
'user-1',
|
||||
);
|
||||
let resolved = await getResolvedOcrConfig(port.id);
|
||||
expect(resolved.manualEntry).toBe(false);
|
||||
|
||||
await saveOcrConfig(
|
||||
port.id,
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', manualEntry: true },
|
||||
'user-1',
|
||||
);
|
||||
resolved = await getResolvedOcrConfig(port.id);
|
||||
expect(resolved.manualEntry).toBe(true);
|
||||
expect(resolved.apiKey).toBe('sk-y'); // toggling the mode never wipes the key
|
||||
});
|
||||
|
||||
it('manualEntry is preserved when other fields change', async () => {
|
||||
const port = await makePort();
|
||||
await saveOcrConfig(
|
||||
port.id,
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: 'sk-z', manualEntry: true },
|
||||
'user-1',
|
||||
);
|
||||
// Update the model only - manualEntry must survive (mirrors aiEnabled).
|
||||
await saveOcrConfig(port.id, { provider: 'openai', model: 'gpt-4o' }, 'user-1');
|
||||
const resolved = await getResolvedOcrConfig(port.id);
|
||||
expect(resolved.manualEntry).toBe(true);
|
||||
expect(resolved.model).toBe('gpt-4o');
|
||||
});
|
||||
|
||||
it('manualEntry shows on the public view and is forced false at global scope', async () => {
|
||||
await saveOcrConfig(
|
||||
null,
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', apiKey: 'g', manualEntry: true },
|
||||
'user-1',
|
||||
);
|
||||
const port = await makePort();
|
||||
const resolved = await getResolvedOcrConfig(port.id);
|
||||
expect(resolved.manualEntry).toBe(false); // per-port, never inherited from global
|
||||
|
||||
await saveOcrConfig(
|
||||
port.id,
|
||||
{ provider: 'openai', model: 'gpt-4o-mini', manualEntry: true },
|
||||
'user-1',
|
||||
);
|
||||
const pub = await getPublicOcrConfig(port.id);
|
||||
expect(pub.manualEntry).toBe(true);
|
||||
});
|
||||
|
||||
it('global rows force useGlobal=false on save (not meaningful at global scope)', async () => {
|
||||
await saveOcrConfig(
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user