Supp/invoice bfl errors (#390)
* feat(accounting): update accounting method validation and messaging for aktiebolag and enskild firma * Remove AI subsystem and related code - Deleted AI proposals and requests persistence logic from `lib/ai/proposals/persist.ts`. - Removed re-validation logic for proposals in `lib/ai/proposals/re-validate.ts`. - Cleaned up schemas related to AI flows in `lib/api/schemas.ts`. - Removed AI-related fields from bookkeeping engine in `lib/bookkeeping/engine.ts`. - Eliminated AI event types from `lib/events/types.ts`. - Updated tests to reflect the removal of AI-related functionality in `lib/extensions/__tests__/sectors.test.ts`. - Adjusted initialization logic in `lib/init.ts` to exclude AI proposal handler registration. - Cleaned up transaction ingestion logic in `lib/transactions/ingest.ts` to remove AI flow checks. - Updated helper functions in `tests/helpers.ts` to remove AI-related settings. - Removed AI-related types and interfaces from `types/index.ts`. - Added migration script to drop AI-related tables and settings from the database. * fix(migrations): ensure foreign key constraint is dropped before removing AI tables * feat(invoice-inbox): implement deterministic invoice field extraction and inbox provisioning - Added `extract-invoice-fields.ts` for extracting fields from PDF invoices using regex and pdfjs-dist, replacing the previous AI classifier. - Introduced `inbox-provisioning.ts` to manage company inbox addresses and rotation of inboxes using Supabase RPCs. - Created `resend-inbound.ts` for handling inbound email events and attachments via the Resend API. - Defined the extension manifest for the invoice inbox, specifying required environment variables and descriptions. - Migrated database schema to remove AI-related columns and tighten the status enum in `invoice_inbox_items`. * feat(invoice-inbox): remove AI-specific columns and tighten status enum * fix(skattekonto): remove manual entry creation reference from transaction input * fix(schemas): remove accounting method validation for aktiebolag in UpdateSettingsSchema
This commit is contained in:
@@ -27,7 +27,7 @@ export const capabilitiesResource: McpResource = {
|
||||
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('bookkeeping_locked_through, vat_registered, pays_salaries, ai_flow_enabled')
|
||||
.select('bookkeeping_locked_through, vat_registered, pays_salaries')
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const companyCurrentResource: McpResource = {
|
||||
accounting_method, default_voucher_series,
|
||||
bookkeeping_locked_through, auto_lock_period_days,
|
||||
invoice_prefix, next_invoice_number, invoice_default_days,
|
||||
is_sandbox, ai_flow_enabled
|
||||
is_sandbox
|
||||
`)
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
generateInvoiceEmailSubject,
|
||||
} from '@/lib/email/invoice-templates'
|
||||
import { uploadDocument, MAX_DOCUMENT_SIZE } from '@/lib/core/documents/document-service'
|
||||
// classifyDocument is dynamically imported from invoice-inbox (may not be enabled)
|
||||
import { extractInvoiceFields } from '@/extensions/general/invoice-inbox/lib/extract-invoice-fields'
|
||||
// ensureInitialized() is called by the extension router (ext/[...path]/route.ts)
|
||||
// which dispatches to this handler — no duplicate call needed here.
|
||||
import type { Transaction, TransactionCategory, EntityType, VatTreatment, Invoice, Currency, CompanySettings, Customer, InvoiceItem } from '@/types'
|
||||
@@ -2298,15 +2298,14 @@ const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_upload_document',
|
||||
description:
|
||||
'Upload a document (invoice, receipt) to the inbox for AI classification.\n\n' +
|
||||
'Upload a document (invoice, receipt) to the inbox. Runs deterministic field extraction (pdfjs + regex) on text-based PDFs.\n\n' +
|
||||
'Args:\n' +
|
||||
' - file_name (string, required): File name with extension (e.g. "faktura.pdf")\n' +
|
||||
' - file_content_base64 (string, required): Base64-encoded file content\n' +
|
||||
' - mime_type (string, optional): MIME type. Inferred from extension if omitted.\n\n' +
|
||||
'Returns JSON:\n' +
|
||||
' { document_id, inbox_item_id, status, document_type, extracted_data, confidence }\n\n' +
|
||||
'Supported types: PDF, JPEG, PNG, HEIC, WebP. Max 20 MB.\n' +
|
||||
'Classification runs synchronously (~2-5 seconds).',
|
||||
' { document_id, inbox_item_id, status, extracted_data }\n\n' +
|
||||
'Supported types: PDF, JPEG, PNG, HEIC, WebP. Max 20 MB.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -2353,53 +2352,42 @@ const tools: McpTool[] = [
|
||||
throw new Error(`File too large (max ${MAX_DOCUMENT_SIZE / 1024 / 1024} MB)`)
|
||||
}
|
||||
|
||||
// Store in WORM archive
|
||||
const doc = await uploadDocument(supabase, userId, companyId, {
|
||||
name: fileName,
|
||||
buffer: buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
||||
type: mimeType,
|
||||
}, { upload_source: 'api' })
|
||||
|
||||
// Classify — skipped when invoice-inbox extension is not enabled
|
||||
// (dynamic import of classify-document pulls @aws-sdk/client-bedrock-runtime which breaks the build)
|
||||
let classificationResult: { documentType?: string; extractedData?: unknown; rawResponse?: unknown; confidence?: number } | undefined
|
||||
let classificationError: string | null = null
|
||||
classificationError = 'invoice-inbox extension not enabled'
|
||||
const { data: extracted } = await extractInvoiceFields({
|
||||
buffer,
|
||||
mimeType,
|
||||
fileName,
|
||||
})
|
||||
|
||||
// Supplier matching
|
||||
let matchedSupplierId: string | null = null
|
||||
if (classificationResult?.documentType === 'supplier_invoice' && classificationResult.extractedData) {
|
||||
const extractedData = classificationResult.extractedData as { supplier?: { orgNumber?: string | null } }
|
||||
const orgNumber = extractedData.supplier?.orgNumber
|
||||
if (orgNumber) {
|
||||
const { data: s } = await supabase
|
||||
.from('suppliers')
|
||||
.select('id')
|
||||
.eq('company_id', companyId)
|
||||
.eq('org_number', orgNumber.replace(/\D/g, ''))
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
if (s) matchedSupplierId = s.id
|
||||
}
|
||||
if (extracted.supplier.orgNumber) {
|
||||
const { data: s } = await supabase
|
||||
.from('suppliers')
|
||||
.select('id')
|
||||
.eq('company_id', companyId)
|
||||
.eq('org_number', extracted.supplier.orgNumber)
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
if (s) matchedSupplierId = s.id
|
||||
}
|
||||
|
||||
// Create inbox item
|
||||
const { data: inbox, error: inboxError } = await supabase
|
||||
.from('invoice_inbox_items')
|
||||
.insert({
|
||||
company_id: companyId,
|
||||
user_id: userId,
|
||||
status: classificationError ? 'error' : 'ready',
|
||||
status: 'received',
|
||||
source: 'upload',
|
||||
document_id: doc.id,
|
||||
document_type: classificationResult?.documentType || 'unknown',
|
||||
extracted_data: classificationResult?.extractedData || null,
|
||||
raw_llm_response: classificationResult?.rawResponse || null,
|
||||
confidence: classificationResult?.confidence ? classificationResult.confidence / 100 : null,
|
||||
extracted_data: extracted as unknown as Record<string, unknown>,
|
||||
matched_supplier_id: matchedSupplierId,
|
||||
error_message: classificationError,
|
||||
})
|
||||
.select('id, status, document_type, confidence')
|
||||
.select('id, status')
|
||||
.single()
|
||||
|
||||
if (inboxError) throw new Error(`Failed to create inbox item: ${inboxError.message}`)
|
||||
@@ -2408,10 +2396,8 @@ const tools: McpTool[] = [
|
||||
document_id: doc.id,
|
||||
inbox_item_id: inbox.id,
|
||||
status: inbox.status,
|
||||
document_type: inbox.document_type,
|
||||
extracted_data: classificationResult?.extractedData || null,
|
||||
confidence: inbox.confidence,
|
||||
error_message: classificationError,
|
||||
extracted_data: extracted,
|
||||
matched_supplier_id: matchedSupplierId,
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -2419,28 +2405,22 @@ const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_list_inbox_items',
|
||||
description:
|
||||
'List document inbox items (classified invoices, receipts, etc.).\n\n' +
|
||||
'List document inbox items (received supplier-invoice documents).\n\n' +
|
||||
'Args:\n' +
|
||||
' - status (string, optional): Filter by status (pending, processing, ready, confirmed, rejected, error)\n' +
|
||||
' - document_type (string, optional): Filter by type (supplier_invoice, receipt, government_letter, unknown)\n' +
|
||||
' - status (string, optional): Filter by status (received, error)\n' +
|
||||
' - limit (number, optional): Max results, 1–50 (default 20)\n\n' +
|
||||
'Returns JSON:\n' +
|
||||
' { items: [{ id, status, document_type, confidence, source, created_at,\n' +
|
||||
' vendor_name, amount, invoice_date, matched_supplier_id }],\n' +
|
||||
' { items: [{ id, status, source, created_at, vendor_name, amount,\n' +
|
||||
' invoice_date, matched_supplier_id, created_supplier_invoice_id }],\n' +
|
||||
' count: number }',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'ready', 'confirmed', 'rejected', 'error'],
|
||||
enum: ['received', 'error'],
|
||||
description: 'Filter by status',
|
||||
},
|
||||
document_type: {
|
||||
type: 'string',
|
||||
enum: ['supplier_invoice', 'receipt', 'government_letter', 'unknown'],
|
||||
description: 'Filter by document type',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max results (default 20, max 50)',
|
||||
@@ -2456,53 +2436,44 @@ const tools: McpTool[] = [
|
||||
async execute(args, companyId, userId, supabase) {
|
||||
const limit = Math.min(Math.max(1, Number(args.limit) || 20), 50)
|
||||
const status = args.status as string | undefined
|
||||
const documentType = args.document_type as string | undefined
|
||||
|
||||
let query = supabase
|
||||
.from('invoice_inbox_items')
|
||||
.select('id, status, document_type, confidence, source, created_at, extracted_data, matched_supplier_id, email_from, email_subject, error_message')
|
||||
.select('id, status, source, created_at, extracted_data, matched_supplier_id, created_supplier_invoice_id, email_from, email_subject, error_message')
|
||||
.eq('company_id', companyId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(limit)
|
||||
|
||||
if (status) query = query.eq('status', status)
|
||||
if (documentType) query = query.eq('document_type', documentType)
|
||||
|
||||
const { data, error } = await query
|
||||
if (error) throw new Error(`Database error: ${error.message}`)
|
||||
|
||||
// Extract key fields from extracted_data for summary
|
||||
const items = (data || []).map((item) => {
|
||||
const extracted = item.extracted_data as Record<string, unknown> | null
|
||||
let vendorName: string | null = null
|
||||
let amount: number | null = null
|
||||
let invoiceDate: string | null = null
|
||||
|
||||
if (extracted && item.document_type === 'supplier_invoice') {
|
||||
if (extracted) {
|
||||
const supplier = extracted.supplier as Record<string, unknown> | undefined
|
||||
const invoice = extracted.invoice as Record<string, unknown> | undefined
|
||||
const totals = extracted.totals as Record<string, unknown> | undefined
|
||||
vendorName = (supplier?.name as string) || null
|
||||
amount = (totals?.total as number) || null
|
||||
invoiceDate = (invoice?.invoiceDate as string) || null
|
||||
} else if (extracted && item.document_type === 'receipt') {
|
||||
const merchant = extracted.merchant as Record<string, unknown> | undefined
|
||||
const totals = extracted.totals as Record<string, unknown> | undefined
|
||||
vendorName = (merchant?.name as string) || null
|
||||
amount = (totals?.total as number) || null
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
status: item.status,
|
||||
document_type: item.document_type,
|
||||
confidence: item.confidence,
|
||||
source: item.source,
|
||||
created_at: item.created_at,
|
||||
vendor_name: vendorName,
|
||||
amount,
|
||||
invoice_date: invoiceDate,
|
||||
matched_supplier_id: item.matched_supplier_id,
|
||||
created_supplier_invoice_id: item.created_supplier_invoice_id,
|
||||
email_from: item.email_from,
|
||||
email_subject: item.email_subject,
|
||||
error_message: item.error_message,
|
||||
@@ -2520,8 +2491,8 @@ const tools: McpTool[] = [
|
||||
'Args:\n' +
|
||||
' - inbox_item_id (string, required): UUID of the inbox item\n\n' +
|
||||
'Returns JSON:\n' +
|
||||
' Full inbox item with id, status, document_type, confidence, source,\n' +
|
||||
' extracted_data (complete), matched_supplier_id, email metadata, timestamps.',
|
||||
' Full inbox item with id, status, source, extracted_data (complete),\n' +
|
||||
' matched_supplier_id, created_supplier_invoice_id, email metadata, timestamps.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
Reference in New Issue
Block a user