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:
@@ -360,21 +360,15 @@ export async function POST(
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm matched inbox item and link its document to the journal entry
|
||||
// Link the matched inbox item's document to the journal entry
|
||||
if (body.inbox_item_id) {
|
||||
try {
|
||||
await supabase
|
||||
.from('invoice_inbox_items')
|
||||
.update({ status: 'confirmed' })
|
||||
.eq('id', body.inbox_item_id)
|
||||
.eq('company_id', companyId)
|
||||
|
||||
// Link inbox item's document to the journal entry
|
||||
if (journalEntryId) {
|
||||
const { data: inboxItem } = await supabase
|
||||
.from('invoice_inbox_items')
|
||||
.select('document_id')
|
||||
.eq('id', body.inbox_item_id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (inboxItem?.document_id) {
|
||||
|
||||
@@ -113,50 +113,6 @@ export async function POST(request: Request) {
|
||||
template_suggestions[tx.id] = [cpSuggestion, ...existing]
|
||||
}
|
||||
|
||||
// Inject document template suggestions from matched inbox items
|
||||
try {
|
||||
const { data: matchedInboxItems } = await supabase
|
||||
.from('invoice_inbox_items')
|
||||
.select('matched_transaction_id, suggested_template_id, suggested_template_confidence')
|
||||
.eq('company_id', companyId)
|
||||
.in('matched_transaction_id', ids)
|
||||
.not('suggested_template_id', 'is', null)
|
||||
|
||||
if (matchedInboxItems && matchedInboxItems.length > 0) {
|
||||
console.log(`[suggest-categories] Found ${matchedInboxItems.length} matched inbox items with template suggestions`)
|
||||
const { getTemplateById } = await import('@/lib/bookkeeping/booking-templates')
|
||||
|
||||
for (const item of matchedInboxItems) {
|
||||
const txId = item.matched_transaction_id as string
|
||||
const templateId = item.suggested_template_id as string
|
||||
const template = getTemplateById(templateId)
|
||||
if (!template) {
|
||||
console.log(`[suggest-categories] Template "${templateId}" not found, skipping`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`[suggest-categories] Injecting document template: tx=${txId} → ${templateId} (${template.name_sv}, debit=${template.debit_account}, confidence=${item.suggested_template_confidence})`)
|
||||
|
||||
// Add to template_suggestions at the top with boosted confidence
|
||||
const existing = template_suggestions[txId] || []
|
||||
const docTemplate: SuggestedTemplate = {
|
||||
template_id: templateId,
|
||||
name_sv: template.name_sv,
|
||||
name_en: template.name_en,
|
||||
group: template.group,
|
||||
debit_account: template.debit_account,
|
||||
credit_account: template.credit_account,
|
||||
confidence: Math.min((item.suggested_template_confidence as number) || 0.8, 1),
|
||||
description_sv: template.description_sv,
|
||||
risk_level: template.risk_level,
|
||||
requires_review: template.requires_review,
|
||||
}
|
||||
template_suggestions[txId] = [docTemplate, ...existing.filter((t) => t.template_id !== templateId)]
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-blocking
|
||||
}
|
||||
|
||||
return NextResponse.json({ suggestions, template_suggestions })
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { ensureInitialized } from '@/lib/init'
|
||||
import { requireCompanyId } from '@/lib/company/context'
|
||||
import { gateAgentInbox } from '@/lib/ai/feature-flag'
|
||||
|
||||
ensureInitialized()
|
||||
|
||||
/**
|
||||
* GET /api/transactions/uncategorized
|
||||
*
|
||||
* Paginated list of uncategorized expense transactions for a picker UI
|
||||
* (e.g. agent-inkorg's "Byt transaktion" flow). Returns expenses only —
|
||||
* amount < 0 — since match proposals always pair receipts to outgoing
|
||||
* payments. Includes basic range filters so callers can narrow to matches
|
||||
* within ±window of a target amount/date.
|
||||
*
|
||||
* Query params:
|
||||
* search Free-text against description/merchant_name (ILIKE)
|
||||
* amount_center Target amount (signed). Must be accompanied by amount_window.
|
||||
* amount_window Half-window in SEK — e.g. 50 means amount_center ± 50.
|
||||
* date_center Target ISO date. Must be accompanied by date_window.
|
||||
* date_window Half-window in days — e.g. 30 means ±30 days.
|
||||
* limit Max rows (1-50, default 20).
|
||||
* offset Row offset for pagination (default 0).
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const gate = gateAgentInbox()
|
||||
if (gate) return gate
|
||||
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
const companyId = await requireCompanyId(supabase, user.id)
|
||||
const url = new URL(request.url)
|
||||
|
||||
const search = url.searchParams.get('search')?.trim() ?? ''
|
||||
const amountCenterRaw = url.searchParams.get('amount_center')
|
||||
const amountWindowRaw = url.searchParams.get('amount_window')
|
||||
const dateCenterRaw = url.searchParams.get('date_center')
|
||||
const dateWindowRaw = url.searchParams.get('date_window')
|
||||
const limit = Math.min(Math.max(1, Number(url.searchParams.get('limit')) || 20), 50)
|
||||
const offset = Math.max(0, Number(url.searchParams.get('offset')) || 0)
|
||||
|
||||
let query = supabase
|
||||
.from('transactions')
|
||||
.select('id, date, description, amount, currency, merchant_name, category, is_business', { count: 'exact' })
|
||||
.eq('company_id', companyId)
|
||||
.is('journal_entry_id', null)
|
||||
.lt('amount', 0)
|
||||
.order('date', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
|
||||
if (search.length > 0) {
|
||||
const escaped = search.replace(/[%_]/g, '\\$&')
|
||||
query = query.or(`description.ilike.%${escaped}%,merchant_name.ilike.%${escaped}%`)
|
||||
}
|
||||
|
||||
if (amountCenterRaw && amountWindowRaw) {
|
||||
const center = Number(amountCenterRaw)
|
||||
const window = Math.abs(Number(amountWindowRaw))
|
||||
if (Number.isFinite(center) && Number.isFinite(window) && window > 0) {
|
||||
query = query.gte('amount', center - window).lte('amount', center + window)
|
||||
}
|
||||
}
|
||||
|
||||
if (dateCenterRaw && dateWindowRaw) {
|
||||
const windowDays = Math.abs(Number(dateWindowRaw))
|
||||
if (Number.isFinite(windowDays) && windowDays > 0) {
|
||||
const center = new Date(dateCenterRaw)
|
||||
if (!Number.isNaN(center.getTime())) {
|
||||
const msPerDay = 86_400_000
|
||||
const from = new Date(center.getTime() - windowDays * msPerDay)
|
||||
const to = new Date(center.getTime() + windowDays * msPerDay)
|
||||
query = query.gte('date', from.toISOString().slice(0, 10))
|
||||
query = query.lte('date', to.toISOString().slice(0, 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error, count } = await query
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
|
||||
return NextResponse.json({
|
||||
data: {
|
||||
transactions: data ?? [],
|
||||
count: count ?? 0,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user