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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user