diff --git a/extensions/general/mcp-server/__tests__/create-supplier-invoice-from-inbox.test.ts b/extensions/general/mcp-server/__tests__/create-supplier-invoice-from-inbox.test.ts index 39882051..493527c7 100644 --- a/extensions/general/mcp-server/__tests__/create-supplier-invoice-from-inbox.test.ts +++ b/extensions/general/mcp-server/__tests__/create-supplier-invoice-from-inbox.test.ts @@ -175,6 +175,29 @@ describe('gnubok_create_supplier_invoice_from_inbox — execute', () => { expect(result.preview.supplier_resolution).toBe('lookup_org_number') }) + it('falls through to name lookup when org_number returns null', async () => { + const supabase = makeMock({ + inbox: { + id: 'inbox-7', + status: 'received', + extracted_data: baseExtracted, + matched_supplier_id: null, + created_supplier_invoice_id: null, + document_id: 'doc-7', + }, + supplierByOrg: null, + supplierByName: { id: 'supplier-name-lookup' }, + }) + const tool = tools.find((t) => t.name === 'gnubok_create_supplier_invoice_from_inbox')! + const result = (await tool.execute( + { inbox_item_id: 'inbox-7', dry_run: true }, + 'company-1', 'user-1', supabase, + )) as { preview: { supplier_resolution: string; supplier_id: string } } + + expect(result.preview.supplier_id).toBe('supplier-name-lookup') + expect(result.preview.supplier_resolution).toBe('lookup_name') + }) + it('throws when inbox item already converted', async () => { const supabase = makeMock({ inbox: { @@ -211,6 +234,39 @@ describe('gnubok_create_supplier_invoice_from_inbox — execute', () => { ).rejects.toThrow(/Cannot resolve supplier/) }) + it('applies line_overrides — overridden account wins over extracted accountSuggestion', async () => { + const extractedWithSuggestion = { + ...baseExtracted, + lineItems: [ + { description: 'Line A', quantity: 1, unit_price: 400, line_total: 400, vat_rate: 25, vat_amount: 100, accountSuggestion: '6550' }, + { description: 'Line B', quantity: 1, unit_price: 600, line_total: 600, vat_rate: 25, vat_amount: 150, accountSuggestion: '6550' }, + ], + } + const supabase = makeMock({ + inbox: { + id: 'inbox-6', + status: 'received', + extracted_data: extractedWithSuggestion, + matched_supplier_id: 'supplier-1', + created_supplier_invoice_id: null, + document_id: 'doc-6', + }, + }) + const tool = tools.find((t) => t.name === 'gnubok_create_supplier_invoice_from_inbox')! + const result = (await tool.execute( + { + inbox_item_id: 'inbox-6', + dry_run: true, + line_overrides: [{ line_number: 2, account_number: '6420' }], + }, + 'company-1', 'user-1', supabase, + )) as { preview: { items_preview: Array<{ line_number: number; account_number: string }> } } + + const items = result.preview.items_preview + expect(items[0].account_number).toBe('6550') // untouched — extracted suggestion wins + expect(items[1].account_number).toBe('6420') // override wins over extracted suggestion + }) + it('throws when extracted_data is missing', async () => { const supabase = makeMock({ inbox: { diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index 8d366bff..61ee43e4 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -6288,6 +6288,19 @@ export const tools: McpTool[] = [ supplier_id_override: { type: 'string', description: 'Force this supplier UUID instead of the matched/extracted one' }, vat_treatment_override: { type: 'string', enum: ['standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt'], description: 'Override extracted VAT treatment' }, due_date_override: { type: 'string', description: 'Override extracted due date (YYYY-MM-DD)' }, + line_overrides: { + type: 'array', + description: 'Per-line account overrides (1-based line_number). Wins over accountSuggestion and supplier default.', + items: { + type: 'object', + additionalProperties: false, + properties: { + line_number: { type: 'number', description: '1-based index matching items_preview' }, + account_number: { type: 'string', description: 'BAS account number for this line (e.g. "6420")' }, + }, + required: ['line_number', 'account_number'], + }, + }, notes: { type: 'string', description: 'Optional notes appended to the supplier invoice' }, dry_run: { type: 'boolean', description: 'If true, return the assembled payload without staging (default false)' }, idempotency_key: { type: 'string', description: 'UUID. Repeat calls with same key + payload return cached response.' }, @@ -6407,19 +6420,26 @@ export const tools: McpTool[] = [ } } + // Build a lookup for per-line account overrides keyed by 1-based line number. + const rawLineOverrides = (args.line_overrides as Array<{ line_number: number; account_number: string }> | undefined) ?? [] + const lineOverrideMap = new Map(rawLineOverrides.map((o) => [o.line_number, o.account_number])) + // Translate extracted line items into the supplier_invoice_items shape. - // Priority: per-line accountSuggestion → supplier.default_expense_account → 4000. - const lineItems = lineItemsExt.map((li, idx) => ({ - line_number: idx + 1, - description: (li.description as string) ?? `Position ${idx + 1}`, - quantity: Number(li.quantity) || 1, - unit: (li.unit as string) ?? 'st', - unit_price: Number(li.unit_price ?? li.unitPrice ?? li.amount) || 0, - line_total: Number(li.line_total ?? li.lineTotal ?? li.amount) || 0, - account_number: (li.accountSuggestion as string | null) ?? supplierDefaultExpenseAccount ?? '4000', - vat_rate: Number(li.vat_rate ?? li.vatRate) || 0, - vat_amount: Number(li.vat_amount ?? li.vatAmount) || 0, - })) + // Priority: line_overrides → per-line accountSuggestion → supplier.default_expense_account → 4000. + const lineItems = lineItemsExt.map((li, idx) => { + const lineNumber = idx + 1 + return { + line_number: lineNumber, + description: (li.description as string) ?? `Position ${lineNumber}`, + quantity: Number(li.quantity) || 1, + unit: (li.unit as string) ?? 'st', + unit_price: Number(li.unit_price ?? li.unitPrice ?? li.amount) || 0, + line_total: Number(li.line_total ?? li.lineTotal ?? li.amount) || 0, + account_number: lineOverrideMap.get(lineNumber) ?? (li.accountSuggestion as string | null) ?? supplierDefaultExpenseAccount ?? '4000', + vat_rate: Number(li.vat_rate ?? li.vatRate) || 0, + vat_amount: Number(li.vat_amount ?? li.vatAmount) || 0, + } + }) const params = { inbox_item_id: inboxItemId,