* fix(dashboard): exclude ignored and already-triaged transactions from stale count The "Gamla transaktioner" widget counted transactions that had been ignored or already marked as is_business=true but not yet booked, so users saw a nag for a row they had already dealt with — and the /transactions inbox correctly hid it. Align the count with the inbox criterion (is_business IS NULL, is_ignored = false) so the widget clears when the row leaves the inbox. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(transactions): read entity_type from settings response wrapper The transactions page read entityRes.entity_type directly, but /api/settings returns { data: { entity_type, ... } }. The expression was always undefined, so setEntityType never fired and entityType stayed at its initial 'enskild_firma'. The template picker's entity_type filter then dropped every aktiebolag-tagged user template for AB customers — only entity_type='all' templates made it through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale templates bank sync journal entry from transaction * fixed pr comments * fixed pr comment --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import type { JournalEntry } from '@/types'
|
|
|
|
export const PAYMENT_SOURCE_TYPES = [
|
|
'invoice_paid',
|
|
'invoice_cash_payment',
|
|
'supplier_invoice_paid',
|
|
'supplier_invoice_cash_payment',
|
|
] as const
|
|
|
|
export function isPaymentSourceType(sourceType: string | null | undefined): boolean {
|
|
if (!sourceType) return false
|
|
return (PAYMENT_SOURCE_TYPES as readonly string[]).includes(sourceType)
|
|
}
|
|
|
|
/**
|
|
* Revert the business-level paid status on the invoice or supplier invoice
|
|
* that a payment journal entry was attached to. Used by both reverseEntry()
|
|
* (storno) and the DELETE journal entry route — both paths leave the GL in a
|
|
* consistent state but the invoice's status/paid_amount/paid_at would otherwise
|
|
* stay stuck on "paid".
|
|
*
|
|
* Safe to call with any entry — returns early if source_type is not a payment.
|
|
*/
|
|
export async function syncInvoiceStatusFromPaymentEntry(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
entry: Pick<JournalEntry, 'id' | 'source_type' | 'source_id'>
|
|
): Promise<void> {
|
|
if (!isPaymentSourceType(entry.source_type) || !entry.source_id) return
|
|
|
|
const entryId = entry.id
|
|
|
|
if (entry.source_type.startsWith('supplier_invoice')) {
|
|
const { data: payment } = await supabase
|
|
.from('supplier_invoice_payments')
|
|
.select('amount')
|
|
.eq('journal_entry_id', entryId)
|
|
.single()
|
|
|
|
const { data: supplierInvoice } = await supabase
|
|
.from('supplier_invoices')
|
|
.select('paid_amount, total_amount, due_date')
|
|
.eq('id', entry.source_id)
|
|
.eq('company_id', companyId)
|
|
.single()
|
|
|
|
if (supplierInvoice && payment) {
|
|
const newPaidAmount = Math.round((supplierInvoice.paid_amount - payment.amount) * 100) / 100
|
|
const newRemaining = Math.round((supplierInvoice.total_amount - Math.max(0, newPaidAmount)) * 100) / 100
|
|
let newStatus: string
|
|
if (newPaidAmount > 0) {
|
|
newStatus = 'partially_paid'
|
|
} else if (supplierInvoice.due_date && new Date(supplierInvoice.due_date) < new Date()) {
|
|
newStatus = 'overdue'
|
|
} else {
|
|
newStatus = 'approved'
|
|
}
|
|
|
|
await supabase
|
|
.from('supplier_invoices')
|
|
.update({
|
|
status: newStatus,
|
|
paid_amount: Math.max(0, newPaidAmount),
|
|
remaining_amount: newRemaining,
|
|
paid_at: null,
|
|
payment_journal_entry_id: null,
|
|
})
|
|
.eq('id', entry.source_id)
|
|
.eq('company_id', companyId)
|
|
}
|
|
} else {
|
|
const { data: payment } = await supabase
|
|
.from('invoice_payments')
|
|
.select('amount')
|
|
.eq('journal_entry_id', entryId)
|
|
.single()
|
|
|
|
const { data: customerInvoice } = await supabase
|
|
.from('invoices')
|
|
.select('paid_amount, due_date')
|
|
.eq('id', entry.source_id)
|
|
.eq('company_id', companyId)
|
|
.single()
|
|
|
|
if (customerInvoice) {
|
|
const paymentAmount = payment?.amount ?? customerInvoice.paid_amount
|
|
const newPaidAmount = Math.round((customerInvoice.paid_amount - paymentAmount) * 100) / 100
|
|
const revertStatus = newPaidAmount > 0
|
|
? 'partially_paid'
|
|
: customerInvoice.due_date && new Date(customerInvoice.due_date) < new Date()
|
|
? 'overdue'
|
|
: 'sent'
|
|
|
|
await supabase
|
|
.from('invoices')
|
|
.update({
|
|
status: revertStatus,
|
|
paid_at: null,
|
|
paid_amount: Math.max(0, newPaidAmount),
|
|
})
|
|
.eq('id', entry.source_id)
|
|
.eq('company_id', companyId)
|
|
.in('status', ['paid', 'partially_paid'])
|
|
}
|
|
}
|
|
}
|