diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx index 1be6fde3..211863e0 100644 --- a/app/(dashboard)/deadlines/page.tsx +++ b/app/(dashboard)/deadlines/page.tsx @@ -145,7 +145,7 @@ export default function DeadlinesPage() { toast({ title: 'Deadline uppdaterad', - description: 'Dina andringar har sparats', + description: 'Dina ändringar har sparats', }) fetchData() diff --git a/app/(dashboard)/invoices/[id]/page.tsx b/app/(dashboard)/invoices/[id]/page.tsx index cfbf62ac..3071929b 100644 --- a/app/(dashboard)/invoices/[id]/page.tsx +++ b/app/(dashboard)/invoices/[id]/page.tsx @@ -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 - )} {invoice.status === 'paid' && isRealInvoice && ( diff --git a/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts b/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts index e950fc4d..3473c180 100644 --- a/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts +++ b/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts @@ -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' diff --git a/app/api/extensions/invoice-inbox/webhook/route.ts b/app/api/extensions/invoice-inbox/webhook/route.ts index 0123f26c..4eaae8bf 100644 --- a/app/api/extensions/invoice-inbox/webhook/route.ts +++ b/app/api/extensions/invoice-inbox/webhook/route.ts @@ -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> | 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), 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 diff --git a/components/extensions/ExtensionWorkspaceLoader.tsx b/components/extensions/ExtensionWorkspaceLoader.tsx index d337e314..745125b6 100644 --- a/components/extensions/ExtensionWorkspaceLoader.tsx +++ b/components/extensions/ExtensionWorkspaceLoader.tsx @@ -24,8 +24,8 @@ export default function ExtensionWorkspaceLoader({ ) : ( )} diff --git a/components/extensions/ExtensionWorkspaceShell.tsx b/components/extensions/ExtensionWorkspaceShell.tsx index f90e016e..9c0e4016 100644 --- a/components/extensions/ExtensionWorkspaceShell.tsx +++ b/components/extensions/ExtensionWorkspaceShell.tsx @@ -20,7 +20,7 @@ export default function ExtensionWorkspaceShell({ {/* Breadcrumb */}