diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts index 314e8358..0296cb66 100644 --- a/app/api/invoices/route.ts +++ b/app/api/invoices/route.ts @@ -182,6 +182,13 @@ export const POST = withRouteContext( vat_amount_sek: documentType === 'delivery_note' ? null : vatAmountSek, total, total_sek: documentType === 'delivery_note' ? null : totalSek, + // Initialize remaining_amount to total for real invoices so the open- + // invoice queries (InvoicePicker, AR ledger, supplier matching) treat + // newly-created invoices as fully unpaid. The DB default is 0 — without + // this, brand-new fakturor look settled and disappear from match + // candidate lists. Proformas and delivery notes have no payment + // obligation, so they keep the 0 default. + remaining_amount: documentType === 'invoice' ? total : 0, vat_treatment: vatRules.treatment, vat_rate: documentType === 'delivery_note' ? 0 : (isMixedRate ? null : (uniqueRates.values().next().value ?? vatRules.rate)), moms_ruta: vatRules.momsRuta, diff --git a/app/api/transactions/[id]/match-invoice/route.ts b/app/api/transactions/[id]/match-invoice/route.ts index 8425aff6..454c8be7 100644 --- a/app/api/transactions/[id]/match-invoice/route.ts +++ b/app/api/transactions/[id]/match-invoice/route.ts @@ -179,6 +179,53 @@ export const POST = withRouteContext( } } + // Underlag for the payment verifikation: re-attach the invoice PDF that + // was archived on send to the new payment journal entry. document_ + // attachments.journal_entry_id is one-to-one, so we insert a parallel + // row pointing at the same storage_path. Same WORM file, second JE + // pointer — no copy, no schema change. Non-blocking (BFL 7 kap audit + // gap, but the bank line + invoice still exist as evidence). + if (journalEntryId && invoice.journal_entry_id) { + try { + const { data: invoiceDoc } = await supabase + .from('document_attachments') + .select('storage_path, file_name, file_size_bytes, mime_type, sha256_hash') + .eq('journal_entry_id', invoice.journal_entry_id) + .eq('company_id', companyId) + .eq('is_current_version', true) + .limit(1) + .maybeSingle() + if (invoiceDoc) { + // Destructure error: Supabase client returns { data, error } on + // postgres-level failures (unique constraint, RLS reject) instead + // of throwing, so the surrounding try/catch only covers thrown + // JS exceptions. Log via warn so attachment failures are visible + // in logs even though we don't abort the match. + const { error: attachErr } = await supabase.from('document_attachments').insert({ + user_id: user.id, + company_id: companyId, + uploaded_by: user.id, + upload_source: 'system', + storage_path: invoiceDoc.storage_path, + file_name: invoiceDoc.file_name, + file_size_bytes: invoiceDoc.file_size_bytes, + mime_type: invoiceDoc.mime_type, + sha256_hash: invoiceDoc.sha256_hash, + journal_entry_id: journalEntryId, + }) + if (attachErr) { + txLog.warn('failed to attach invoice PDF to payment journal entry', { + attachError: attachErr.message, + paymentJournalEntryId: journalEntryId, + invoiceJournalEntryId: invoice.journal_entry_id, + }) + } + } + } catch (err) { + txLog.warn('failed to attach invoice PDF to payment journal entry', err as Error) + } + } + // Optimistic lock: only update if invoice is still in a matchable state. const { data: updatedRows, error: updateInvError } = await supabase .from('invoices')