refactor: consolidate AI analyzers into shared lib/ai module and add journal entry reversal columns
Extract shared vision/document analysis logic into lib/ai (vision-client, document-analyzer, image preprocessing, validation helpers), simplifying invoice-analyzer, receipt-analyzer, and document classifier. Add migration 046 for journal entry reversal/correction link columns (reversed_by_id, reverses_id, correction_of_id) required by storno service. Update extension components and shared UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6d647b956c
commit
3f255ea201
@@ -145,7 +145,7 @@ export default function DeadlinesPage() {
|
||||
|
||||
toast({
|
||||
title: 'Deadline uppdaterad',
|
||||
description: 'Dina andringar har sparats',
|
||||
description: 'Dina ändringar har sparats',
|
||||
})
|
||||
|
||||
fetchData()
|
||||
|
||||
@@ -181,8 +181,21 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || 'Kunde inte markera som betald')
|
||||
}
|
||||
} else if (status === 'cancelled') {
|
||||
// Only drafts and proformas can be cancelled directly — sent/overdue/paid
|
||||
// invoices have committed journal entries and require a credit note instead
|
||||
if (invoice.status !== 'draft') {
|
||||
const docType = ((invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice') as InvoiceDocumentType
|
||||
if (docType !== 'proforma') {
|
||||
throw new Error('Bokförda fakturor kan inte makuleras. Skapa en kreditfaktura istället.')
|
||||
}
|
||||
}
|
||||
const { error } = await supabase
|
||||
.from('invoices')
|
||||
.update({ status })
|
||||
.eq('id', invoice.id)
|
||||
if (error) throw new Error(error.message)
|
||||
} else {
|
||||
// Other status changes (cancelled) — direct update is fine
|
||||
const { error } = await supabase
|
||||
.from('invoices')
|
||||
.update({ status })
|
||||
@@ -870,15 +883,6 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
Skapa kreditfaktura
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => updateStatus('cancelled')}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
Makulera
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{invoice.status === 'paid' && isRealInvoice && (
|
||||
|
||||
@@ -31,9 +31,9 @@ vi.mock('@/extensions/general/invoice-inbox/lib/email-handler', () => ({
|
||||
resolveUserFromEmail: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock invoice analyzer
|
||||
vi.mock('@/extensions/general/invoice-inbox/lib/invoice-analyzer', () => ({
|
||||
analyzeInvoice: vi.fn(),
|
||||
// Mock unified document analyzer
|
||||
vi.mock('@/lib/ai/document-analyzer', () => ({
|
||||
analyzeDocument: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock supplier matcher
|
||||
@@ -41,6 +41,11 @@ vi.mock('@/extensions/general/invoice-inbox/lib/supplier-matcher', () => ({
|
||||
matchSupplier: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock receipt pipeline
|
||||
vi.mock('@/extensions/general/receipt-ocr/lib/receipt-pipeline', () => ({
|
||||
processReceiptFromDocument: vi.fn(),
|
||||
}))
|
||||
|
||||
import { POST } from '../route'
|
||||
import { parseInboundPayload, extractAttachments, resolveUserFromEmail } from '@/extensions/general/invoice-inbox/lib/email-handler'
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { Webhook } from 'svix'
|
||||
import { parseInboundPayload, extractAttachments, resolveUserFromEmail } from '@/extensions/general/invoice-inbox/lib/email-handler'
|
||||
import { analyzeInvoice } from '@/extensions/general/invoice-inbox/lib/invoice-analyzer'
|
||||
import { matchSupplier } from '@/extensions/general/invoice-inbox/lib/supplier-matcher'
|
||||
import { classifyDocument } from '@/lib/documents/classifier'
|
||||
import { analyzeDocument } from '@/lib/ai/document-analyzer'
|
||||
import { processReceiptFromDocument } from '@/extensions/general/receipt-ocr/lib/receipt-pipeline'
|
||||
import crypto from 'crypto'
|
||||
|
||||
@@ -145,14 +144,13 @@ export async function POST(request: Request) {
|
||||
|
||||
if (docError || !document) continue
|
||||
|
||||
// Classify document type
|
||||
// Unified classify + extract in a single Claude call
|
||||
let documentType: 'supplier_invoice' | 'receipt' | 'government_letter' | 'unknown' = 'supplier_invoice'
|
||||
let isReverseCharge = false
|
||||
let unifiedResult: Awaited<ReturnType<typeof analyzeDocument>> | null = null
|
||||
try {
|
||||
const classification = await classifyDocument(attachment.content, attachment.content_type)
|
||||
documentType = classification.type
|
||||
isReverseCharge = classification.isReverseCharge ?? false
|
||||
console.log(`[document-inbox] Classified as ${documentType} (confidence: ${classification.confidence})`)
|
||||
unifiedResult = await analyzeDocument(attachment.content, attachment.content_type)
|
||||
documentType = unifiedResult.classification.type
|
||||
console.log(`[document-inbox] Classified as ${documentType} (confidence: ${unifiedResult.classification.confidence})`)
|
||||
} catch (classifyErr) {
|
||||
console.error('[document-inbox] Classification failed, defaulting to supplier_invoice:', classifyErr)
|
||||
}
|
||||
@@ -180,10 +178,15 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
switch (documentType) {
|
||||
case 'supplier_invoice': {
|
||||
// Existing flow: analyze invoice + supplier match
|
||||
const extraction = await analyzeInvoice(attachment.content, attachment.content_type)
|
||||
// Use pre-extracted invoice data from unified call
|
||||
const extraction = unifiedResult?.invoice
|
||||
if (!extraction) {
|
||||
throw new Error('No invoice extraction available')
|
||||
}
|
||||
|
||||
// Store reverse charge flag from classifier in extracted data
|
||||
const isReverseCharge = unifiedResult?.classification.isReverseCharge ?? false
|
||||
|
||||
// Store reverse charge flag in extracted data
|
||||
const extractedData = {
|
||||
...(extraction as unknown as Record<string, unknown>),
|
||||
isReverseCharge,
|
||||
@@ -216,7 +219,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
case 'receipt': {
|
||||
// Receipt pipeline: extract + categorize + match transactions
|
||||
// Use pre-extracted receipt data from unified call
|
||||
const { data: urlData } = supabase.storage.from('documents').getPublicUrl(storagePath)
|
||||
|
||||
const result = await processReceiptFromDocument(supabase, userId, attachment.content, attachment.content_type, {
|
||||
@@ -224,6 +227,7 @@ export async function POST(request: Request) {
|
||||
source: 'email',
|
||||
emailFrom: payload.from,
|
||||
storageUrl: urlData.publicUrl,
|
||||
preExtracted: unifiedResult?.receipt ?? undefined,
|
||||
})
|
||||
|
||||
await supabase
|
||||
|
||||
Reference in New Issue
Block a user