Fix/supp ag fb (#1023)

* fix: prevent credit notes from entering payment flow

* fix: persist and display customer personal numbers

* feat: configure automatic invoice reminder days

* fix: issue credit notes through send flow

* chore: add repository agent guidance

* feat(mcp): route tools across user companies

* fix(articles): delete unused register entries

* feat(invoices): improve issued invoice actions

* feat(supplier-invoices): retain uploaded source documents

* docs: record implementation decisions

* feat: enhance customer personal number handling and validation

- Updated CustomerForm to allow personal numbers in the format of "********-1234" for individual customers.
- Added validation to ensure personal numbers are only accepted for individual customers in CreateCustomerSchema.
- Implemented masking and encryption for personal numbers to enhance data protection.
- Introduced new utility functions for masking and encrypting personal numbers.
- Added database migration to enforce unique constraints on credit note relationships and prevent duplicate entries.
- Enhanced error handling and logging for credit note issuance and invoice processing.
- Updated tests to cover new credit note creation guards and personal number handling.

* test: enhance list companies test with supabase query mocks
This commit is contained in:
Mattsson
2026-07-15 15:53:15 +02:00
committed by GitHub
parent a558c75678
commit 072aedeaf9
116 changed files with 5708 additions and 669 deletions
@@ -52,6 +52,23 @@ describe('getJournalEntryUnderlagReferences', () => {
expect(refs).toEqual([{ type: 'supplier_invoice', id: 'si-1', number: 'LF-001' }])
})
it('surfaces the retained PDF through a supplier payment reference', async () => {
const refs = await run([
{ data: [] }, // 1. invoices direct
{ data: [] }, // 2. invoice_payments
{ data: [] }, // 4. supplier registration
{ data: [{ id: 'si-1', supplier_invoice_number: 'LF-001', document_id: 'doc-1' }] }, // 5. supplier payment
{ data: [{ supplier_invoice_id: 'si-1' }] }, // 6. supplier_invoice_payments
])
expect(refs).toEqual([{
type: 'supplier_invoice',
id: 'si-1',
number: 'LF-001',
document_id: 'doc-1',
}])
})
it('returns nothing when no invoice is linked (warning legitimately stays)', async () => {
const refs = await run([
{ data: [] }, // 1. invoices direct
@@ -1,4 +1,5 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
/**
* A followable reference from a verifikation back to its underlag: the customer
@@ -20,6 +21,8 @@ export interface UnderlagReference {
id: string
/** invoice_number / supplier_invoice_number: the UI builds the label from this. */
number: string
/** Retained source document owned by a referenced supplier invoice, if any. */
document_id?: string
}
interface InvoiceRow {
@@ -30,6 +33,7 @@ interface InvoiceRow {
interface SupplierInvoiceRow {
id: string
supplier_invoice_number: string
document_id?: string | null
}
/**
@@ -52,21 +56,21 @@ export async function getJournalEntryUnderlagReferences(
const invoices = new Map<string, string>()
// Direct link (faktureringsmetod registration, or invoices.journal_entry_id).
const { data: directInvoices } = await supabase
.from('invoices')
.select('id, invoice_number')
.eq('company_id', companyId)
.eq('journal_entry_id', journalEntryId)
const directInvoices = await fetchAllRows<InvoiceRow>(({ from, to }) =>
supabase.from('invoices').select('id, invoice_number')
.eq('company_id', companyId).eq('journal_entry_id', journalEntryId)
.order('id', { ascending: true }).range(from, to),
)
for (const inv of (directInvoices ?? []) as InvoiceRow[]) {
invoices.set(inv.id, inv.invoice_number)
}
// Payment rows (kontantmetod inbetalning, partial payments) → invoice_payments.
const { data: paymentRows } = await supabase
.from('invoice_payments')
.select('invoice_id')
.eq('journal_entry_id', journalEntryId)
const paymentRows = await fetchAllRows<{ id: string; invoice_id: string | null }>(({ from, to }) =>
supabase.from('invoice_payments').select('id, invoice_id')
.eq('journal_entry_id', journalEntryId).order('id', { ascending: true }).range(from, to),
)
const paymentInvoiceIds = new Set<string>()
for (const row of (paymentRows ?? []) as { invoice_id: string | null }[]) {
@@ -74,11 +78,11 @@ export async function getJournalEntryUnderlagReferences(
}
if (paymentInvoiceIds.size > 0) {
const { data: paidInvoices } = await supabase
.from('invoices')
.select('id, invoice_number')
.eq('company_id', companyId)
.in('id', Array.from(paymentInvoiceIds))
const paidInvoices = await fetchAllRows<InvoiceRow>(({ from, to }) =>
supabase.from('invoices').select('id, invoice_number')
.eq('company_id', companyId).in('id', Array.from(paymentInvoiceIds))
.order('id', { ascending: true }).range(from, to),
)
for (const inv of (paidInvoices ?? []) as InvoiceRow[]) {
invoices.set(inv.id, inv.invoice_number)
@@ -86,35 +90,41 @@ export async function getJournalEntryUnderlagReferences(
}
// --- Supplier invoices ---------------------------------------------------
const supplierInvoices = new Map<string, string>()
const supplierInvoices = new Map<string, { number: string; documentId?: string }>()
// Registration booking (accrual) on the invoice itself.
const { data: registrationLinks } = await supabase
.from('supplier_invoices')
.select('id, supplier_invoice_number')
.eq('company_id', companyId)
.eq('registration_journal_entry_id', journalEntryId)
const registrationLinks = await fetchAllRows<SupplierInvoiceRow>(({ from, to }) =>
supabase.from('supplier_invoices').select('id, supplier_invoice_number, document_id')
.eq('company_id', companyId).eq('registration_journal_entry_id', journalEntryId)
.order('id', { ascending: true }).range(from, to),
)
for (const si of (registrationLinks ?? []) as SupplierInvoiceRow[]) {
supplierInvoices.set(si.id, si.supplier_invoice_number)
supplierInvoices.set(si.id, {
number: si.supplier_invoice_number,
...(si.document_id ? { documentId: si.document_id } : {}),
})
}
// Payment booking on the invoice itself.
const { data: paymentLinks } = await supabase
.from('supplier_invoices')
.select('id, supplier_invoice_number')
.eq('company_id', companyId)
.eq('payment_journal_entry_id', journalEntryId)
const paymentLinks = await fetchAllRows<SupplierInvoiceRow>(({ from, to }) =>
supabase.from('supplier_invoices').select('id, supplier_invoice_number, document_id')
.eq('company_id', companyId).eq('payment_journal_entry_id', journalEntryId)
.order('id', { ascending: true }).range(from, to),
)
for (const si of (paymentLinks ?? []) as SupplierInvoiceRow[]) {
supplierInvoices.set(si.id, si.supplier_invoice_number)
supplierInvoices.set(si.id, {
number: si.supplier_invoice_number,
...(si.document_id ? { documentId: si.document_id } : {}),
})
}
// Partial-payment rows → supplier_invoice_payments.
const { data: supplierPaymentRows } = await supabase
.from('supplier_invoice_payments')
.select('supplier_invoice_id')
.eq('journal_entry_id', journalEntryId)
const supplierPaymentRows = await fetchAllRows<{ id: string; supplier_invoice_id: string | null }>(
({ from, to }) => supabase.from('supplier_invoice_payments').select('id, supplier_invoice_id')
.eq('journal_entry_id', journalEntryId).order('id', { ascending: true }).range(from, to),
)
const supplierPaymentIds = new Set<string>()
for (const row of (supplierPaymentRows ?? []) as { supplier_invoice_id: string | null }[]) {
@@ -124,20 +134,30 @@ export async function getJournalEntryUnderlagReferences(
}
if (supplierPaymentIds.size > 0) {
const { data: paidSupplierInvoices } = await supabase
.from('supplier_invoices')
.select('id, supplier_invoice_number')
.eq('company_id', companyId)
.in('id', Array.from(supplierPaymentIds))
const paidSupplierInvoices = await fetchAllRows<SupplierInvoiceRow>(({ from, to }) =>
supabase.from('supplier_invoices').select('id, supplier_invoice_number, document_id')
.eq('company_id', companyId).in('id', Array.from(supplierPaymentIds))
.order('id', { ascending: true }).range(from, to),
)
for (const si of (paidSupplierInvoices ?? []) as SupplierInvoiceRow[]) {
supplierInvoices.set(si.id, si.supplier_invoice_number)
supplierInvoices.set(si.id, {
number: si.supplier_invoice_number,
...(si.document_id ? { documentId: si.document_id } : {}),
})
}
}
// --- Assemble ------------------------------------------------------------
const references: UnderlagReference[] = []
for (const [id, number] of invoices) references.push({ type: 'invoice', id, number })
for (const [id, number] of supplierInvoices) references.push({ type: 'supplier_invoice', id, number })
for (const [id, supplierInvoice] of supplierInvoices) {
references.push({
type: 'supplier_invoice',
id,
number: supplierInvoice.number,
...(supplierInvoice.documentId ? { document_id: supplierInvoice.documentId } : {}),
})
}
return references
}