Files
accounted/lib/invoices/ensure-invoice-number.ts
T
Mattsson 3e42fc6f32 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
2026-06-04 13:14:57 +02:00

53 lines
1.9 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
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
}
/**
* Assign an invoice number to a draft invoice. Idempotent: if the row already
* has a number, returns it unchanged without consuming a sequence number.
*
* Concurrency is handled inside the generate_invoice_number RPC via row lock.
* Two callers racing on the same draft both return the same final number; the
* counter advances by exactly one. Proforma document_type produces a 'PF-'
* prefix; everything else uses the company's configured invoice_prefix.
*/
export async function ensureInvoiceNumber(
supabase: SupabaseClient,
companyId: string,
invoice: InvoiceShape,
): Promise<string> {
if (invoice.invoice_number) {
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,
p_document_type: invoice.document_type ?? 'invoice',
})
if (rpcError || !assigned) {
throw new Error(`Failed to assign invoice number: ${rpcError?.message ?? 'no value returned'}`)
}
invoice.invoice_number = assigned
return assigned
}