fix(invoices): init remaining_amount on create + attach invoice PDF to payment JE (#406)
* fix(invoices): initialize remaining_amount on create The invoices table column remaining_amount has DB default 0. The create path never set it, so brand-new fakturor were stored with remaining_amount=0 even though no payment had been received. The InvoicePicker (and any future open-invoice query that filters on remaining_amount > 0) treated these as fully settled and hid them from match candidates — surfaced when a real user reported "Inga öppna fakturor" despite having 5 sent invoices. Set remaining_amount = total on insert for document_type='invoice'. Proformas and delivery notes have no payment obligation, so they keep the 0 default. Backfill of the 46 existing rows across 20 companies (1.32M SEK in orphaned receivables) ran separately as a one-shot UPDATE — restricted to rows with paid_amount IS NULL OR 0 so any legitimately-paid invoice with stale status was untouched (verified: 0 such rows). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(match-invoice): attach invoice PDF as underlag for payment JE The payment verifikation created on transaction match (debit 1930 / credit 1510) had no document attachment. The invoice PDF was archived on send and pinned to the AR-booking JE, but document_attachments .journal_entry_id is one-to-one — the payment JE was left without underlag, a BFL 7 kap audit-trail gap. Cheapest fix: after the payment JE is created, look up the invoice's existing document_attachment row and insert a parallel row that points at the same storage_path with the new journal_entry_id. The original WORM file is untouched (single storage object, two DB pointers); no schema change. Wrapped in non-blocking try/catch so a document lookup failure doesn't abort the payment match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(match-invoice): destructure document_attachments insert error Supabase JS client returns { data, error } on Postgres-level failures (unique constraint, RLS reject) instead of throwing. The surrounding try/catch only caught thrown JS exceptions, so DB errors on the payment JE document attachment were silently swallowed — the txLog.warn path was unreachable for the most likely failure mode. Destructure { error: attachErr } and log on error with both JE ids so attachment failures are visible and reparable. Greptile P1 on PR #406. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
97db09a3ff
commit
30f5877e57
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user