* fix: prevent credit notes from entering payment flow * fix: persist and display customer personal numbers * feat: configure automatic invoice reminder days * fix: issue credit notes through send flow * chore: add repository agent guidance * feat(mcp): route tools across user companies * fix(articles): delete unused register entries * feat(invoices): improve issued invoice actions * feat(supplier-invoices): retain uploaded source documents * docs: record implementation decisions * feat: enhance customer personal number handling and validation - Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers. - Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema. - Implemented masking and encryption for personal numbers to enhance data protection. - Introduced new utility functions for masking and encrypting personal numbers. - Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries. - Enhanced error handling and logging for credit note issuance and invoice processing. - Updated tests to cover new credit note creation guards and personal number handling. * test: enhance list companies test with supabase query mocks
201 lines
7.4 KiB
TypeScript
201 lines
7.4 KiB
TypeScript
/**
|
|
* Tests for the VAT review widget: registration, resource serving,
|
|
* and tool _meta wiring. Does NOT re-test the underlying VAT computation
|
|
* (covered by existing get_vat_report tests); only the widget plumbing.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { tools } from '../server'
|
|
import { uiWidgets, findUiWidget } from '../widgets'
|
|
|
|
vi.mock('@/lib/supabase/server', () => ({
|
|
createClient: vi.fn(),
|
|
createServiceClient: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/auth/api-keys', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/lib/auth/api-keys')>()
|
|
return {
|
|
...actual,
|
|
extractBearerToken: vi.fn().mockReturnValue('test-token'),
|
|
validateApiKey: vi.fn().mockResolvedValue({
|
|
userId: 'user-1',
|
|
companyId: '11111111-1111-4111-8111-111111111111',
|
|
scopes: ['reports:read'],
|
|
}),
|
|
// Fully-chainable, awaitable proxy resolving to empty data: satisfies both
|
|
// loadAtomsAsSkills (select→eq→eq→order) and computeVatReport (select→eq→in→
|
|
// gte→lte→…) without hand-enumerating each chain.
|
|
createServiceClientNoCookies: vi.fn(() => {
|
|
const makeChain = (): unknown =>
|
|
new Proxy(
|
|
{},
|
|
{
|
|
get(_t, prop) {
|
|
if (prop === 'then') {
|
|
return (resolve: (v: unknown) => void) => resolve({ data: [], error: null, count: 0 })
|
|
}
|
|
return () => makeChain()
|
|
},
|
|
},
|
|
)
|
|
const membershipChain: unknown = new Proxy(
|
|
{},
|
|
{
|
|
get(_t, prop) {
|
|
if (prop === 'then') {
|
|
return (resolve: (v: unknown) => void) =>
|
|
resolve({
|
|
data: {
|
|
company_id: '11111111-1111-4111-8111-111111111111',
|
|
role: 'owner',
|
|
},
|
|
error: null,
|
|
})
|
|
}
|
|
return () => membershipChain
|
|
},
|
|
}
|
|
)
|
|
return {
|
|
from: (table: string) => (table === 'company_members' ? membershipChain : makeChain()),
|
|
}
|
|
}),
|
|
}
|
|
})
|
|
|
|
import { handleMcpRequest } from '../server'
|
|
|
|
function mcpRequest(method: string, params?: Record<string, unknown>, id: number | string = 1): Request {
|
|
return new Request('http://localhost:3000/api/extensions/ext/mcp-server/mcp', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' },
|
|
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
})
|
|
}
|
|
|
|
async function parseResult(response: Response) {
|
|
const json = await response.json()
|
|
return json.result
|
|
}
|
|
|
|
describe('VAT review widget', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('widget registration', () => {
|
|
it('registers the vat-review widget in uiWidgets', () => {
|
|
const widget = findUiWidget('ui://vat-review/app.html')
|
|
expect(widget).toBeDefined()
|
|
expect(widget?.name).toBe('VAT Review')
|
|
expect(widget?.html).toContain('<!DOCTYPE html>')
|
|
expect(widget?.html).toContain('Momsdeklaration')
|
|
})
|
|
|
|
it('uiWidgets contains both receipt-matcher and vat-review', () => {
|
|
const uris = uiWidgets.map((w) => w.uri)
|
|
expect(uris).toContain('ui://receipt-matcher/app.html')
|
|
expect(uris).toContain('ui://vat-review/app.html')
|
|
})
|
|
})
|
|
|
|
describe('gnubok_vat_review_widget tool', () => {
|
|
it('is registered with _meta.ui pointing to the vat-review widget', () => {
|
|
const tool = tools.find((t) => t.name === 'gnubok_vat_review_widget')
|
|
expect(tool).toBeDefined()
|
|
expect(tool?._meta).toEqual({ ui: { resourceUri: 'ui://vat-review/app.html' } })
|
|
expect(tool?.annotations.readOnlyHint).toBe(true)
|
|
})
|
|
|
|
it('declares the same required inputs as gnubok_get_vat_report', () => {
|
|
const widgetTool = tools.find((t) => t.name === 'gnubok_vat_review_widget')
|
|
const reportTool = tools.find((t) => t.name === 'gnubok_get_vat_report')
|
|
const widgetRequired = (widgetTool?.inputSchema as { required?: string[] }).required ?? []
|
|
const reportRequired = (reportTool?.inputSchema as { required?: string[] }).required ?? []
|
|
expect(widgetRequired.sort()).toEqual(reportRequired.sort())
|
|
})
|
|
})
|
|
|
|
describe('protocol: resources/list', () => {
|
|
it('lists the vat-review widget alongside the receipt-matcher widget', async () => {
|
|
const res = await handleMcpRequest(mcpRequest('resources/list'))
|
|
const result = await parseResult(res)
|
|
|
|
const widget = result.resources.find(
|
|
(r: { uri: string }) => r.uri === 'ui://vat-review/app.html'
|
|
)
|
|
expect(widget).toEqual({
|
|
uri: 'ui://vat-review/app.html',
|
|
name: 'VAT Review',
|
|
description: 'Interactive review of momsdeklaration (SKV 4700) before filing',
|
|
mimeType: 'text/html;profile=mcp-app',
|
|
})
|
|
|
|
const uris = result.resources.map((r: { uri: string }) => r.uri)
|
|
expect(uris).toContain('ui://receipt-matcher/app.html')
|
|
expect(uris).toContain('ui://vat-review/app.html')
|
|
})
|
|
})
|
|
|
|
describe('protocol: resources/read', () => {
|
|
it('returns HTML for the vat-review widget', async () => {
|
|
const res = await handleMcpRequest(
|
|
mcpRequest('resources/read', { uri: 'ui://vat-review/app.html' })
|
|
)
|
|
const result = await parseResult(res)
|
|
|
|
expect(result.contents).toHaveLength(1)
|
|
expect(result.contents[0].uri).toBe('ui://vat-review/app.html')
|
|
expect(result.contents[0].mimeType).toBe('text/html;profile=mcp-app')
|
|
expect(result.contents[0].text).toContain('Momsdeklaration')
|
|
expect(result.contents[0].text).toContain('ruta49')
|
|
})
|
|
})
|
|
|
|
describe('protocol: tools/list', () => {
|
|
it('includes gnubok_vat_review_widget with _meta when the API key has reports:read scope', async () => {
|
|
const res = await handleMcpRequest(mcpRequest('tools/list'))
|
|
const result = await parseResult(res)
|
|
|
|
const widgetTool = result.tools.find(
|
|
(t: { name: string }) => t.name === 'gnubok_vat_review_widget'
|
|
)
|
|
expect(widgetTool).toBeDefined()
|
|
expect(widgetTool._meta).toEqual({ ui: { resourceUri: 'ui://vat-review/app.html' } })
|
|
})
|
|
})
|
|
|
|
describe('gnubok_get_vat_report: render_ui merge', () => {
|
|
it('declares render_ui and points at the vat-review widget', () => {
|
|
const tool = tools.find((t) => t.name === 'gnubok_get_vat_report')!
|
|
expect((tool as { uiResourceUri?: string }).uiResourceUri).toBe('ui://vat-review/app.html')
|
|
const props = (tool.inputSchema as { properties: Record<string, unknown> }).properties
|
|
expect(props.render_ui).toMatchObject({ type: 'boolean' })
|
|
})
|
|
|
|
it('emits result-level _meta only when render_ui=true', async () => {
|
|
const withUi = await (
|
|
await handleMcpRequest(
|
|
mcpRequest('tools/call', {
|
|
name: 'gnubok_get_vat_report',
|
|
arguments: { period_type: 'monthly', year: 2026, period: 3, render_ui: true },
|
|
}),
|
|
)
|
|
).json()
|
|
expect(withUi.result.isError).toBeUndefined()
|
|
expect(withUi.result._meta).toEqual({ ui: { resourceUri: 'ui://vat-review/app.html' } })
|
|
|
|
const withoutUi = await (
|
|
await handleMcpRequest(
|
|
mcpRequest('tools/call', {
|
|
name: 'gnubok_get_vat_report',
|
|
arguments: { period_type: 'monthly', year: 2026, period: 3 },
|
|
}),
|
|
)
|
|
).json()
|
|
expect(withoutUi.result.isError).toBeUndefined()
|
|
expect(withoutUi.result._meta).toBeUndefined()
|
|
})
|
|
})
|
|
})
|