diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index bc6ec7fc..58119570 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -35,6 +35,16 @@ export default async function DashboardLayout({
.eq('user_id', user.id)
.eq('enabled', true)
+ // Check if ai-chat is explicitly disabled (legacy default: enabled when no row exists)
+ const { data: aiChatToggle } = await supabase
+ .from('extension_toggles')
+ .select('enabled')
+ .eq('user_id', user.id)
+ .eq('sector_slug', 'general')
+ .eq('extension_slug', 'ai-chat')
+ .single()
+ const showAiChat = aiChatToggle ? aiChatToggle.enabled : true
+
return (
{/* Skip to content link for keyboard/screen reader users */}
@@ -54,7 +64,7 @@ export default async function DashboardLayout({
{children}
-
+ {showAiChat && }
)
}
diff --git a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
index 93e4dabb..318f7d77 100644
--- a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
@@ -12,6 +12,10 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
+vi.mock('@/lib/init', () => ({
+ ensureInitialized: vi.fn(),
+}))
+
const mockCreateJournalEntry = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
diff --git a/app/api/bookkeeping/journal-entries/route.ts b/app/api/bookkeeping/journal-entries/route.ts
index 9c447af6..9366da98 100644
--- a/app/api/bookkeeping/journal-entries/route.ts
+++ b/app/api/bookkeeping/journal-entries/route.ts
@@ -1,8 +1,11 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { createJournalEntry } from '@/lib/bookkeeping/engine'
+import { ensureInitialized } from '@/lib/init'
import type { CreateJournalEntryInput } from '@/types'
+ensureInitialized()
+
export async function GET(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
diff --git a/app/api/extensions/ai-categorization/settings/route.ts b/app/api/extensions/ai-categorization/settings/route.ts
index 9a76b945..38ec4e63 100644
--- a/app/api/extensions/ai-categorization/settings/route.ts
+++ b/app/api/extensions/ai-categorization/settings/route.ts
@@ -1,6 +1,9 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getSettings, saveSettings } from '@/extensions/general/ai-categorization'
+import { ensureInitialized } from '@/lib/init'
+
+ensureInitialized()
/**
* GET /api/extensions/ai-categorization/settings
diff --git a/app/api/extensions/ai-categorization/suggestions/route.ts b/app/api/extensions/ai-categorization/suggestions/route.ts
index c08a91c0..1b7a624f 100644
--- a/app/api/extensions/ai-categorization/suggestions/route.ts
+++ b/app/api/extensions/ai-categorization/suggestions/route.ts
@@ -1,8 +1,11 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { categorizeTransactions } from '@/extensions/general/ai-categorization'
+import { ensureInitialized } from '@/lib/init'
import type { CategorizationSuggestion } from '@/extensions/general/ai-categorization/categorizer'
+ensureInitialized()
+
/**
* GET /api/extensions/ai-categorization/suggestions?transaction_ids=id1,id2,...
* Fetch pre-computed AI suggestions for given transaction IDs
diff --git a/app/api/extensions/enable-banking/sync/cron/route.ts b/app/api/extensions/enable-banking/sync/cron/route.ts
index 025b436d..017beb38 100644
--- a/app/api/extensions/enable-banking/sync/cron/route.ts
+++ b/app/api/extensions/enable-banking/sync/cron/route.ts
@@ -2,8 +2,11 @@ import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
import { syncAccountTransactions } from '@/extensions/general/enable-banking/lib/sync'
import { isConsentExpiringSoon, getDaysUntilExpiry } from '@/extensions/general/enable-banking/lib/api-client'
+import { ensureInitialized } from '@/lib/init'
import type { StoredAccount } from '@/extensions/general/enable-banking/types'
+ensureInitialized()
+
/**
* GET /api/extensions/enable-banking/sync/cron
* Automatic daily bank transaction sync
diff --git a/app/api/extensions/receipt-ocr/upload/route.ts b/app/api/extensions/receipt-ocr/upload/route.ts
index 7f70eaa9..215792dd 100644
--- a/app/api/extensions/receipt-ocr/upload/route.ts
+++ b/app/api/extensions/receipt-ocr/upload/route.ts
@@ -143,6 +143,8 @@ export async function POST(request: Request) {
vat_amount: item.vatRate && item.lineTotal ? (item.lineTotal * item.vatRate) / (100 + item.vatRate) : null,
extraction_confidence: item.confidence,
suggested_category: item.suggestedCategory,
+ category: item.category,
+ bas_account: item.basAccount,
sort_order: index,
}))
diff --git a/app/api/transactions/[id]/categorize/__tests__/route.test.ts b/app/api/transactions/[id]/categorize/__tests__/route.test.ts
index 4246c696..35b2c1d7 100644
--- a/app/api/transactions/[id]/categorize/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/categorize/__tests__/route.test.ts
@@ -128,7 +128,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
// Fetch company settings
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
// ensureFiscalPeriod: check existing
- enqueue({ data: { id: 'period-1' }, error: null })
+ enqueue({ data: [{ id: 'period-1' }], error: null })
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
mockSaveUserMappingRule.mockResolvedValue(undefined)
@@ -177,7 +177,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
enqueue({ data: tx, error: null })
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
- enqueue({ data: { id: 'period-1' }, error: null })
+ enqueue({ data: [{ id: 'period-1' }], error: null })
mockCreateTransactionJournalEntry.mockRejectedValue(new Error('Period locked'))
@@ -210,7 +210,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
enqueue({ data: tx, error: null })
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
- enqueue({ data: { id: 'period-1' }, error: null })
+ enqueue({ data: [{ id: 'period-1' }], error: null })
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
@@ -237,7 +237,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
enqueue({ data: tx, error: null })
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
- enqueue({ data: { id: 'period-1' }, error: null })
+ enqueue({ data: [{ id: 'period-1' }], error: null })
mockCreateTransactionJournalEntry.mockResolvedValue({ id: 'je-1' })
diff --git a/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts b/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
index 92cee214..99271abe 100644
--- a/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
@@ -14,14 +14,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
-const mockCreateJournalEntry = vi.fn()
-const mockFindFiscalPeriod = vi.fn()
-vi.mock('@/lib/bookkeeping/engine', () => ({
- createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
- findFiscalPeriod: (...args: unknown[]) => mockFindFiscalPeriod(...args),
-}))
-
+const mockCreateInvoicePaymentJournalEntry = vi.fn()
+const mockCreateInvoiceCashEntry = vi.fn()
vi.mock('@/lib/bookkeeping/invoice-entries', () => ({
+ createInvoicePaymentJournalEntry: (...args: unknown[]) => mockCreateInvoicePaymentJournalEntry(...args),
+ createInvoiceCashEntry: (...args: unknown[]) => mockCreateInvoiceCashEntry(...args),
getRevenueAccount: vi.fn().mockReturnValue('3001'),
getOutputVatAccount: vi.fn().mockReturnValue('2611'),
}))
@@ -35,7 +32,6 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
vi.clearAllMocks()
reset()
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
- mockFindFiscalPeriod.mockResolvedValue('period-1')
})
it('returns 401 when not authenticated', async () => {
@@ -161,7 +157,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
// Fetch company settings
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
- mockCreateJournalEntry.mockResolvedValue({ id: 'je-1' })
+ mockCreateInvoicePaymentJournalEntry.mockResolvedValue({ id: 'je-1' })
// Update invoice to paid
enqueue({ data: null, error: null })
@@ -186,16 +182,11 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
expect(body.paid_amount).toBe(12500)
expect(body.journal_entry_id).toBe('je-1')
- // Verify accrual journal entry: debit 1930, credit 1510
- expect(mockCreateJournalEntry).toHaveBeenCalledWith(
+ // Verify accrual payment entry was called
+ expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
'user-1',
- expect.objectContaining({
- source_type: 'invoice_paid',
- lines: expect.arrayContaining([
- expect.objectContaining({ account_number: '1930', debit_amount: 12500 }),
- expect.objectContaining({ account_number: '1510', credit_amount: 12500 }),
- ]),
- })
+ expect.objectContaining({ id: 'inv-1' }),
+ '2024-06-15'
)
})
@@ -207,7 +198,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
enqueue({ data: invoice, error: null })
enqueue({ data: { accounting_method: 'accrual', entity_type: 'enskild_firma' }, error: null })
- mockCreateJournalEntry.mockRejectedValue(new Error('Period locked'))
+ mockCreateInvoicePaymentJournalEntry.mockRejectedValue(new Error('Period locked'))
// Update invoice
enqueue({ data: null, error: null })
diff --git a/extensions/general/push-notifications/index.ts b/extensions/general/push-notifications/index.ts
index 14e53bb4..3faab849 100644
--- a/extensions/general/push-notifications/index.ts
+++ b/extensions/general/push-notifications/index.ts
@@ -45,16 +45,22 @@ export async function getSettings(userId: string): Promise) }
+ return {
+ periodLockedEnabled: data.period_locked_enabled ?? DEFAULT_SETTINGS.periodLockedEnabled,
+ periodYearClosedEnabled: data.period_year_closed_enabled ?? DEFAULT_SETTINGS.periodYearClosedEnabled,
+ invoiceSentEnabled: data.invoice_sent_enabled ?? DEFAULT_SETTINGS.invoiceSentEnabled,
+ receiptExtractedEnabled: data.receipt_extracted_enabled ?? DEFAULT_SETTINGS.receiptExtractedEnabled,
+ receiptMatchedEnabled: data.receipt_matched_enabled ?? DEFAULT_SETTINGS.receiptMatchedEnabled,
+ }
}
export async function saveSettings(
@@ -67,15 +73,17 @@ export async function saveSettings(
const supabase = await createClient()
await supabase
- .from('extension_data')
+ .from('notification_settings')
.upsert(
{
user_id: userId,
- extension_id: 'push-notifications',
- key: 'settings',
- value: merged,
+ period_locked_enabled: merged.periodLockedEnabled,
+ period_year_closed_enabled: merged.periodYearClosedEnabled,
+ invoice_sent_enabled: merged.invoiceSentEnabled,
+ receipt_extracted_enabled: merged.receiptExtractedEnabled,
+ receipt_matched_enabled: merged.receiptMatchedEnabled,
},
- { onConflict: 'user_id,extension_id,key' }
+ { onConflict: 'user_id' }
)
return merged
@@ -231,6 +239,21 @@ export const pushNotificationsExtension: Extension = {
path: '/settings/extensions/push-notifications',
},
async onInstall(ctx) {
- await saveSettings(ctx.userId, DEFAULT_SETTINGS)
+ const supabase = await createClient()
+
+ // Ensure a notification_settings row exists for this user
+ await supabase
+ .from('notification_settings')
+ .upsert(
+ {
+ user_id: ctx.userId,
+ period_locked_enabled: DEFAULT_SETTINGS.periodLockedEnabled,
+ period_year_closed_enabled: DEFAULT_SETTINGS.periodYearClosedEnabled,
+ invoice_sent_enabled: DEFAULT_SETTINGS.invoiceSentEnabled,
+ receipt_extracted_enabled: DEFAULT_SETTINGS.receiptExtractedEnabled,
+ receipt_matched_enabled: DEFAULT_SETTINGS.receiptMatchedEnabled,
+ },
+ { onConflict: 'user_id' }
+ )
},
}
diff --git a/extensions/general/push-notifications/types.ts b/extensions/general/push-notifications/types.ts
index 40faeafb..5018f8bf 100644
--- a/extensions/general/push-notifications/types.ts
+++ b/extensions/general/push-notifications/types.ts
@@ -21,6 +21,11 @@ export interface NotificationSettings {
quiet_end: string // time format "HH:MM"
email_enabled: boolean
push_enabled: boolean
+ period_locked_enabled: boolean
+ period_year_closed_enabled: boolean
+ invoice_sent_enabled: boolean
+ receipt_extracted_enabled: boolean
+ receipt_matched_enabled: boolean
created_at: string
updated_at: string
}
diff --git a/extensions/general/receipt-ocr/index.ts b/extensions/general/receipt-ocr/index.ts
index 80234ec9..ba06d504 100644
--- a/extensions/general/receipt-ocr/index.ts
+++ b/extensions/general/receipt-ocr/index.ts
@@ -174,6 +174,8 @@ async function handleDocumentUploaded(
: null,
extraction_confidence: item.confidence,
suggested_category: item.suggestedCategory,
+ category: item.category,
+ bas_account: item.basAccount,
sort_order: index,
}))
diff --git a/lib/bookkeeping/invoice-entries.ts b/lib/bookkeeping/invoice-entries.ts
index 440c8e55..a430149b 100644
--- a/lib/bookkeeping/invoice-entries.ts
+++ b/lib/bookkeeping/invoice-entries.ts
@@ -1,6 +1,6 @@
import { createJournalEntry, findFiscalPeriod } from './engine'
import { generateSalesVatLines, generateReverseChargeLines } from './vat-entries'
-import { getVatTreatmentForRate } from '@/lib/invoice/vat-rules'
+import { getVatTreatmentForRate } from '@/lib/invoices/vat-rules'
import type {
CreateJournalEntryInput,
CreateJournalEntryLineInput,
diff --git a/lib/reports/__tests__/balance-sheet.test.ts b/lib/reports/__tests__/balance-sheet.test.ts
index 31888595..69a8bcf0 100644
--- a/lib/reports/__tests__/balance-sheet.test.ts
+++ b/lib/reports/__tests__/balance-sheet.test.ts
@@ -159,9 +159,14 @@ describe('generateBalanceSheet', () => {
const report = await generateBalanceSheet('user-1', 'period-1')
expect(report.asset_sections).toHaveLength(1) // Only 1930
- expect(report.equity_liability_sections).toEqual([])
+ // Class 3-8 accounts are not included as balance sheet rows, but their
+ // net result (credit - debit = 40000 + 500 - 8000 = 32500) appears as
+ // "Årets resultat" in equity so the balance sheet can balance.
+ expect(report.equity_liability_sections).toHaveLength(1)
+ expect(report.equity_liability_sections[0].title).toBe('Årets resultat')
+ expect(report.equity_liability_sections[0].subtotal).toBe(32500)
expect(report.total_assets).toBe(50000)
- expect(report.total_equity_liabilities).toBe(0)
+ expect(report.total_equity_liabilities).toBe(32500)
})
it('uses Math.round for monetary precision on subtotals', async () => {
diff --git a/supabase/migrations/20240101000033_ai_chat_schema.sql b/supabase/migrations/20240101000033_ai_chat_schema.sql
new file mode 100644
index 00000000..c21dc34f
--- /dev/null
+++ b/supabase/migrations/20240101000033_ai_chat_schema.sql
@@ -0,0 +1,127 @@
+-- Migration 033: AI Chat Schema
+-- Creates tables for the AI chat assistant extension:
+-- chat_sessions, chat_messages, knowledge_documents, and match_documents RPC
+
+-- Enable pgvector for embedding storage
+create extension if not exists vector with schema extensions;
+
+-- ============================================================
+-- chat_sessions
+-- ============================================================
+
+create table public.chat_sessions (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid references auth.users on delete cascade not null,
+ title text,
+ created_at timestamptz not null default now(),
+ updated_at timestamptz not null default now()
+);
+
+alter table public.chat_sessions enable row level security;
+
+create policy "chat_sessions_select" on public.chat_sessions
+ for select using (auth.uid() = user_id);
+create policy "chat_sessions_insert" on public.chat_sessions
+ for insert with check (auth.uid() = user_id);
+create policy "chat_sessions_update" on public.chat_sessions
+ for update using (auth.uid() = user_id);
+create policy "chat_sessions_delete" on public.chat_sessions
+ for delete using (auth.uid() = user_id);
+
+create index idx_chat_sessions_user_created on public.chat_sessions (user_id, created_at desc);
+
+create trigger chat_sessions_updated_at
+ before update on public.chat_sessions
+ for each row execute function public.update_updated_at_column();
+
+-- ============================================================
+-- chat_messages
+-- ============================================================
+
+create table public.chat_messages (
+ id uuid primary key default gen_random_uuid(),
+ session_id uuid references public.chat_sessions on delete cascade not null,
+ user_id uuid references auth.users on delete cascade not null,
+ role text not null check (role in ('user', 'assistant')),
+ content text not null,
+ sources jsonb,
+ created_at timestamptz not null default now()
+);
+
+alter table public.chat_messages enable row level security;
+
+create policy "chat_messages_select" on public.chat_messages
+ for select using (auth.uid() = user_id);
+create policy "chat_messages_insert" on public.chat_messages
+ for insert with check (auth.uid() = user_id);
+create policy "chat_messages_update" on public.chat_messages
+ for update using (auth.uid() = user_id);
+create policy "chat_messages_delete" on public.chat_messages
+ for delete using (auth.uid() = user_id);
+
+create index idx_chat_messages_session on public.chat_messages (session_id, created_at);
+
+-- ============================================================
+-- knowledge_documents
+-- ============================================================
+
+create table public.knowledge_documents (
+ id uuid primary key default gen_random_uuid(),
+ source_file text not null,
+ title text not null,
+ section_title text,
+ content text not null,
+ content_hash text unique not null,
+ embedding extensions.vector(1536),
+ metadata jsonb default '{}',
+ created_at timestamptz not null default now()
+);
+
+alter table public.knowledge_documents enable row level security;
+
+-- Knowledge documents are shared — any authenticated user can read
+create policy "knowledge_documents_select" on public.knowledge_documents
+ for select using (true);
+
+create index idx_knowledge_documents_hash on public.knowledge_documents (content_hash);
+
+-- ============================================================
+-- match_documents RPC (vector similarity search)
+-- ============================================================
+
+create or replace function public.match_documents(
+ query_embedding extensions.vector,
+ match_count int default 5,
+ match_threshold float default 0.7
+)
+returns table (
+ id uuid,
+ source_file text,
+ title text,
+ section_title text,
+ content text,
+ metadata jsonb,
+ similarity float
+)
+language plpgsql
+security definer
+set search_path = public, extensions
+as $$
+begin
+ return query
+ select
+ kd.id,
+ kd.source_file,
+ kd.title,
+ kd.section_title,
+ kd.content,
+ kd.metadata,
+ 1 - (kd.embedding <=> query_embedding)::float as similarity
+ from public.knowledge_documents kd
+ where 1 - (kd.embedding <=> query_embedding) >= match_threshold
+ order by kd.embedding <=> query_embedding
+ limit match_count;
+end;
+$$;
+
+grant execute on function public.match_documents(extensions.vector, int, float) to authenticated;
diff --git a/supabase/migrations/20240101000034_fix_extension_data_trigger.sql b/supabase/migrations/20240101000034_fix_extension_data_trigger.sql
new file mode 100644
index 00000000..6bfd0b85
--- /dev/null
+++ b/supabase/migrations/20240101000034_fix_extension_data_trigger.sql
@@ -0,0 +1,19 @@
+-- Migration 034: Fix extension_data updated_at trigger
+-- The original trigger references update_updated_at() which does not exist.
+-- The correct function is public.update_updated_at_column().
+-- Wrapped in DO block in case extension_data table does not yet exist.
+
+do $$
+begin
+ if exists (
+ select 1 from information_schema.tables
+ where table_schema = 'public' and table_name = 'extension_data'
+ ) then
+ drop trigger if exists extension_data_updated_at on public.extension_data;
+
+ create trigger extension_data_updated_at
+ before update on public.extension_data
+ for each row execute function public.update_updated_at_column();
+ end if;
+end;
+$$;
diff --git a/supabase/migrations/20240101000035_fix_push_notifications.sql b/supabase/migrations/20240101000035_fix_push_notifications.sql
new file mode 100644
index 00000000..a12388db
--- /dev/null
+++ b/supabase/migrations/20240101000035_fix_push_notifications.sql
@@ -0,0 +1,28 @@
+-- Migration 035: Fix push notifications
+-- 1. Expand notification_log notification_type CHECK constraint to include new event types
+-- 2. Add per-event enabled columns to notification_settings
+
+-- Drop and recreate the CHECK constraint with new types
+alter table public.notification_log
+ drop constraint if exists notification_log_notification_type_check;
+
+alter table public.notification_log
+ add constraint notification_log_notification_type_check
+ check (notification_type in (
+ 'tax_deadline',
+ 'invoice_due',
+ 'invoice_overdue',
+ 'period_locked',
+ 'period_year_closed',
+ 'invoice_sent',
+ 'receipt_extracted',
+ 'receipt_matched'
+ ));
+
+-- Add new per-event enabled columns to notification_settings
+alter table public.notification_settings
+ add column if not exists period_locked_enabled boolean default true,
+ add column if not exists period_year_closed_enabled boolean default true,
+ add column if not exists invoice_sent_enabled boolean default false,
+ add column if not exists receipt_extracted_enabled boolean default true,
+ add column if not exists receipt_matched_enabled boolean default true;
diff --git a/supabase/migrations/20240101000036_fix_enable_banking.sql b/supabase/migrations/20240101000036_fix_enable_banking.sql
new file mode 100644
index 00000000..74d5269e
--- /dev/null
+++ b/supabase/migrations/20240101000036_fix_enable_banking.sql
@@ -0,0 +1,5 @@
+-- Migration 036: Fix Enable Banking
+-- Add authorization_id column to bank_connections for PSD2 authorization tracking
+
+alter table public.bank_connections
+ add column if not exists authorization_id text;
diff --git a/supabase/migrations/20240101000029_extension_toggles.sql b/supabase/migrations/20240101000037_extension_toggles.sql
similarity index 100%
rename from supabase/migrations/20240101000029_extension_toggles.sql
rename to supabase/migrations/20240101000037_extension_toggles.sql
diff --git a/supabase/migrations/20240101000038_fix_match_documents_search_path.sql b/supabase/migrations/20240101000038_fix_match_documents_search_path.sql
new file mode 100644
index 00000000..26cb1bf3
--- /dev/null
+++ b/supabase/migrations/20240101000038_fix_match_documents_search_path.sql
@@ -0,0 +1,37 @@
+-- Migration 038: Fix match_documents search_path
+-- The function needs the extensions schema in search_path to use pgvector operators.
+
+create or replace function public.match_documents(
+ query_embedding extensions.vector,
+ match_count int default 5,
+ match_threshold float default 0.7
+)
+returns table (
+ id uuid,
+ source_file text,
+ title text,
+ section_title text,
+ content text,
+ metadata jsonb,
+ similarity float
+)
+language plpgsql
+security definer
+set search_path = public, extensions
+as $$
+begin
+ return query
+ select
+ kd.id,
+ kd.source_file,
+ kd.title,
+ kd.section_title,
+ kd.content,
+ kd.metadata,
+ 1 - (kd.embedding <=> query_embedding)::float as similarity
+ from public.knowledge_documents kd
+ where 1 - (kd.embedding <=> query_embedding) >= match_threshold
+ order by kd.embedding <=> query_embedding
+ limit match_count;
+end;
+$$;