feat(mcp): unify missing-document surfaces on one predicate (#876)
* feat(mcp): unify missing-document surfaces on one predicate (P1-3) The two MCP surfaces told different truths: the transactions tool keyed 'has underlag' on transactions.document_id while the verifikat tool keyed on document_attachments — and neither respected the source-type semantics, version chains, or journal_entry_no_doc_required waivers that lib/worklist's canonical count applies. Measured on prod: 22,046 waived verifikat still listed to agents, 2,370 doc-exempt source types listed, ~87 docs attached to transactions but never propagated to the verifikat, 1,100 transactions flagged missing-receipt although their verifikat HAS the underlag. One predicate now lives in SQL — posted, needs-doc source type (mirrors NEEDS_DOC_SOURCE_TYPES), no current-version doc, no waiver: - verifikat_without_documents RPC v2 adopts the canonical predicate. - New transactions_without_documents RPC: the bank-driven subset of the same predicate, joined through transactions.journal_entry_id — a strict subset of the verifikat surface by construction. Rows expose qualified transaction_id (P1-2 forward-compat); bare id deprecated. - Both tools become thin RPC wrappers; descriptions state the actual set relationship. - lib/worklist countVerifikatMissingDocument delegates to the RPC (previously three full-table pulls set-differenced client-side) — badge count and agent surfaces can no longer drift. - Backfill: propagate transaction-attached docs to their verifikat where the attachment was never linked (open periods only; never steals a doc linked to another verifikat). pg-real: fixture matrix (no-doc/with-doc/waived/stale-version/ doc-exempt-source/import), strict-subset assertion, per-source-type pin of the SQL list against the TS constant, tenant guard. Part of dev_docs/mcp_optimization_plan.md (P1-3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(mcp): explicit grants restated + count-call comment (#876 review) - Restate REVOKE/GRANT on verifikat_without_documents so the migration is self-contained (CREATE OR REPLACE preserves the 20260703130000 grants — verified on prod: authenticated + service_role only). - Comment on the p_limit:1 count call: total_count is computed over the full filtered set, independent of page size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
250cc7c450
commit
21512db81a
@@ -78,57 +78,27 @@ describe('countInboxDocuments', () => {
|
||||
})
|
||||
|
||||
describe('countVerifikatMissingDocument', () => {
|
||||
it('counts posted document-requiring entries with neither document nor exemption', async () => {
|
||||
// 6 posted entries: je-1 documented+exempt, je-2 documented, je-3 exempt
|
||||
// → je-4, je-5, je-6 missing.
|
||||
enqueue({
|
||||
data: [
|
||||
{ id: 'je-1' },
|
||||
{ id: 'je-2' },
|
||||
{ id: 'je-3' },
|
||||
{ id: 'je-4' },
|
||||
{ id: 'je-5' },
|
||||
{ id: 'je-6' },
|
||||
],
|
||||
})
|
||||
enqueue({
|
||||
data: [
|
||||
{ journal_entry_id: 'je-1' },
|
||||
{ journal_entry_id: 'je-1' }, // second doc on the same entry — still one entry
|
||||
{ journal_entry_id: 'je-2' },
|
||||
],
|
||||
})
|
||||
enqueue({
|
||||
data: [{ journal_entry_id: 'je-1' }, { journal_entry_id: 'je-3' }],
|
||||
})
|
||||
// Predicate semantics (needs-doc source types, current versions, waivers)
|
||||
// now live in the verifikat_without_documents RPC and are pinned by
|
||||
// tests/pg/document-surfaces-unification.pg.test.ts against real Postgres.
|
||||
// These tests cover only the delegation contract.
|
||||
it('delegates to the verifikat_without_documents RPC and returns its total', async () => {
|
||||
enqueue({ data: { ok: true, total_count: 3, verifikat: [] }, error: null })
|
||||
await expect(countVerifikatMissingDocument(supabase, COMPANY)).resolves.toBe(3)
|
||||
expect(mockSupabase.rpc).toHaveBeenCalledWith('verifikat_without_documents', {
|
||||
p_company_id: COMPANY,
|
||||
p_limit: 1,
|
||||
p_offset: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores documents attached to entries outside the document-requiring set', async () => {
|
||||
// The doc on je-99 (e.g. a VAT-settlement entry) must not shrink the count.
|
||||
enqueue({ data: [{ id: 'je-1' }] })
|
||||
enqueue({ data: [{ journal_entry_id: 'je-99' }] })
|
||||
enqueue({ data: [] })
|
||||
await expect(countVerifikatMissingDocument(supabase, COMPANY)).resolves.toBe(1)
|
||||
})
|
||||
|
||||
it('soft-fails to 0 when a paginated read errors', async () => {
|
||||
enqueue({ error: { message: 'boom' } })
|
||||
enqueue({ data: [] })
|
||||
enqueue({ data: [] })
|
||||
it('soft-fails to 0 when the RPC errors', async () => {
|
||||
enqueue({ data: null, error: { message: 'boom' } })
|
||||
await expect(countVerifikatMissingDocument(supabase, COMPANY)).resolves.toBe(0)
|
||||
})
|
||||
|
||||
it('soft-fails to 0 (never a silent partial) when pagination errors mid-stream', async () => {
|
||||
// First page of entries is full (1000 = fetchAllRows page size), so a
|
||||
// second page is requested and errors. fetchAllRows must throw — the
|
||||
// count drops to a logged 0 rather than computing from a truncated set.
|
||||
enqueue({
|
||||
data: Array.from({ length: 1000 }, (_, i) => ({ id: `je-${i}` })),
|
||||
})
|
||||
enqueue({ data: [] }) // document_attachments page 1
|
||||
enqueue({ data: [] }) // exemptions page 1
|
||||
enqueue({ error: { message: 'mid-stream failure' } }) // entries page 2
|
||||
it('soft-fails to 0 on a not-ok envelope (tenant guard)', async () => {
|
||||
enqueue({ data: { ok: false, code: 'VERIFIKAT_WITHOUT_DOCUMENTS_FORBIDDEN' }, error: null })
|
||||
await expect(countVerifikatMissingDocument(supabase, COMPANY)).resolves.toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user