Files
accounted/lib/bookkeeping/payment-sync.ts
T
MattssonandClaude Opus 4.7 ea1bf01f1e Fix/m sprint fixes (#613)
* 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>
2026-05-30 01:28:41 +02:00

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'])
}
}
}