* 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>
44 lines
1.2 KiB
TypeScript
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
|
|
}
|
|
}
|