Feat/voucher docs (#664)

* feat: implement inbox document picker and linking functionality

* feat: implement self-billing invoice functionality

- Added support for registering self-billed invoices received from customers.
- Updated the invoice schema to include fields for self-billing metadata such as `is_self_billed`, `external_invoice_number`, `self_billing_agreement_ref`, and `received_date`.
- Created API route for handling self-billed invoice submissions, including validation and error handling.
- Implemented database migrations to add necessary columns and constraints for self-billing invoices.
- Developed tests to ensure correct behavior of self-billing invoice creation and validation rules.
- Updated Swedish localization files to include new terms related to self-billing.

* feat: enforce SIE import requirement for non-Fortnox providers in migration process

* feat: streamline invoice processing and enhance error logging across APIs
This commit is contained in:
Mattsson
2026-06-04 13:14:57 +02:00
committed by GitHub
parent c1be9f15dd
commit 3e42fc6f32
45 changed files with 2675 additions and 529 deletions
@@ -86,4 +86,34 @@ describe('ensureInvoiceNumber', () => {
ensureInvoiceNumber(supabase as never, 'company-1', invoice)
).rejects.toThrow('no value returned')
})
it('returns the external number for a self-billed invoice without touching the RPC', async () => {
// A received self-billing invoice carries the counterparty's number; we must
// never consume our own löpnummerserie (BFL 5 kap 6§).
const invoice = {
id: 'inv-1',
invoice_number: null,
is_self_billed: true,
external_invoice_number: 'KUND-55012',
}
const result = await ensureInvoiceNumber(supabase as never, 'company-1', invoice)
expect(result).toBe('KUND-55012')
expect(supabase.rpc).not.toHaveBeenCalled()
})
it('throws when a self-billed invoice is missing the external number', async () => {
const invoice = {
id: 'inv-1',
invoice_number: null,
is_self_billed: true,
external_invoice_number: null,
}
await expect(
ensureInvoiceNumber(supabase as never, 'company-1', invoice)
).rejects.toThrow('missing external_invoice_number')
expect(supabase.rpc).not.toHaveBeenCalled()
})
})
+13
View File
@@ -3,3 +3,16 @@ export const INVOICE_NUMBER_DRAFT_LABEL = '(Utkast)'
export function invoiceNumberDisplay(value: string | null | undefined): string {
return value ?? INVOICE_NUMBER_DRAFT_LABEL
}
/**
* The number to show for an invoice. Self-billing invoices we received carry
* the counterparty's number in `external_invoice_number` (our own
* `invoice_number` is null by design), so fall back to it before the draft
* label.
*/
export function invoiceDisplayNumber(invoice: {
invoice_number?: string | null
external_invoice_number?: string | null
}): string {
return invoice.invoice_number ?? invoice.external_invoice_number ?? INVOICE_NUMBER_DRAFT_LABEL
}
+13
View File
@@ -4,6 +4,8 @@ import type { Invoice, InvoiceDocumentType } from '@/types'
type InvoiceShape = Pick<Invoice, 'id' | 'invoice_number'> & {
invoice_number: string | null
document_type?: InvoiceDocumentType | null
is_self_billed?: boolean | null
external_invoice_number?: string | null
}
/**
@@ -24,6 +26,17 @@ export async function ensureInvoiceNumber(
return invoice.invoice_number
}
// Self-billing invoices we received carry the COUNTERPARTY's number; we must
// never consume our own löpnummerserie for them (BFL 5 kap 6§). The DB
// constraint invoices_self_billed_numbering guarantees the external number is
// present, but guard here so a future caller can't silently mint an F-number.
if (invoice.is_self_billed) {
if (!invoice.external_invoice_number) {
throw new Error('Self-billed invoice is missing external_invoice_number')
}
return invoice.external_invoice_number
}
const { data: assigned, error: rpcError } = await supabase.rpc('generate_invoice_number', {
p_company_id: companyId,
p_invoice_id: invoice.id,