Files
accounted/lib/invoices/match-log.ts
T
Jakob WennbergandClaude Opus 4.6 7c3518a080 feat: payment matching audit trail and partial payments (#89)
* feat: payment matching audit trail, partial payments, and match improvements

- Add payment_match_log table (append-only audit trail per BFL 7:1) with
  immutability triggers and proper RLS
- Add invoice_payments table for partial payment tracking, mirroring
  supplier_invoice_payments pattern
- Add remaining_amount column + partially_paid status to invoices
- Add match-log.ts service for recording match/unmatch state transitions
- Improve match-invoice route: support partial payments, record audit log,
  emit payment.matched events, handle storno conflicts
- Improve match-supplier-invoice route: audit logging, partial payment support
- Update invoice-entries.ts for partial payment journal entries
- Update transaction ingest to detect and auto-suggest invoice matches
- Update bank-reconciliation to handle partial payment state
- Add invoice_payments, payment_match_log types and helpers
- Extend tests for match-invoice route and transaction ingest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback — payment matching correctness bugs

- P1: Return 500 on non-23505 payment insert failures instead of
  silently continuing with corrupted invoice state (both routes)
- P1: Fix foreign-currency partial payment matching — use proportional
  remaining SEK amount instead of full total_sek
- P2: Check error when clearing journal_entry_id after storno
- P2: Add updated_at column + trigger to invoice_payments table
- P2: Make supplier_invoice_payments.user_id NOT NULL after backfill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:00:37 +01:00

44 lines
1.2 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
type MatchAction =
| 'matched'
| 'unmatched'
| 'auto_suggested'
| 'suggestion_cleared'
| 'storno_conflict_resolved'
/**
* Log a payment match event to the append-only audit trail.
* Fire-and-forget — never throws, never blocks the caller.
*/
export async function logMatchEvent(
supabase: SupabaseClient,
userId: string,
transactionId: string,
action: MatchAction,
opts?: {
invoiceId?: string
supplierInvoiceId?: string
matchConfidence?: number
matchMethod?: string
previousState?: Record<string, unknown>
newState?: Record<string, unknown>
}
): Promise<void> {
try {
await supabase.from('payment_match_log').insert({
user_id: userId,
transaction_id: transactionId,
invoice_id: opts?.invoiceId ?? null,
supplier_invoice_id: opts?.supplierInvoiceId ?? null,
action,
match_confidence: opts?.matchConfidence ?? null,
match_method: opts?.matchMethod ?? null,
previous_state: opts?.previousState ?? null,
new_state: opts?.newState ?? null,
})
} catch {
// Non-critical — audit log insert must never break the main flow
}
}