3e42fc6f32
* 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
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getPool } from '@/tests/pg/setup'
|
|
import { seedCompany } from '@/tests/pg/fixtures'
|
|
|
|
/**
|
|
* Constraints added by 20260613100000_self_billing_received_invoices.sql:
|
|
* - invoices_self_billed_numbering: a self-billed row carries the
|
|
* counterparty's number in external_invoice_number and never an own one.
|
|
* - invoices_sent_requires_number: loosened so a self-billed row may be 'sent'
|
|
* with a NULL own invoice_number (its löpnummer is the external number).
|
|
*/
|
|
describe('self-billing invoice constraints', () => {
|
|
async function insertSelfBilled(params: {
|
|
userId: string
|
|
companyId: string
|
|
status?: string
|
|
invoiceNumber?: string | null
|
|
externalNumber?: string | null
|
|
isSelfBilled?: boolean
|
|
}): Promise<string> {
|
|
const id = randomUUID()
|
|
const customerId = randomUUID()
|
|
await getPool().query(
|
|
`INSERT INTO public.customers (id, user_id, company_id, name)
|
|
VALUES ($1, $2, $3, 'Stora Bolaget AB')`,
|
|
[customerId, params.userId, params.companyId],
|
|
)
|
|
await getPool().query(
|
|
`INSERT INTO public.invoices
|
|
(id, user_id, company_id, customer_id, invoice_number,
|
|
is_self_billed, external_invoice_number, received_date,
|
|
invoice_date, due_date, currency, subtotal, vat_amount, total,
|
|
vat_treatment, vat_rate, moms_ruta, status)
|
|
VALUES ($1, $2, $3, $4, $5,
|
|
$6, $7, '2026-06-02',
|
|
'2026-06-01', '2026-06-30', 'SEK', 10000, 2500, 12500,
|
|
'standard_25', 25, '05', $8)`,
|
|
[
|
|
id,
|
|
params.userId,
|
|
params.companyId,
|
|
customerId,
|
|
params.invoiceNumber ?? null,
|
|
params.isSelfBilled ?? true,
|
|
params.externalNumber ?? null,
|
|
params.status ?? 'sent',
|
|
],
|
|
)
|
|
return id
|
|
}
|
|
|
|
it('accepts a self-billed sale: external number set, own number null, status sent', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
|
|
const id = await insertSelfBilled({
|
|
userId,
|
|
companyId,
|
|
externalNumber: 'KUND-55012',
|
|
invoiceNumber: null,
|
|
status: 'sent',
|
|
})
|
|
|
|
const { rows } = await getPool().query<{ is_self_billed: boolean; external_invoice_number: string }>(
|
|
'SELECT is_self_billed, external_invoice_number FROM public.invoices WHERE id = $1',
|
|
[id],
|
|
)
|
|
expect(rows[0]!.is_self_billed).toBe(true)
|
|
expect(rows[0]!.external_invoice_number).toBe('KUND-55012')
|
|
})
|
|
|
|
it('rejects a self-billed row that also carries an own invoice_number', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
|
|
await expect(
|
|
insertSelfBilled({ userId, companyId, externalNumber: 'KUND-55012', invoiceNumber: 'F-2026001' }),
|
|
).rejects.toThrow(/invoices_self_billed_numbering/i)
|
|
})
|
|
|
|
it('rejects a self-billed row with no external_invoice_number', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
|
|
await expect(
|
|
insertSelfBilled({ userId, companyId, externalNumber: null, invoiceNumber: null }),
|
|
).rejects.toThrow(/invoices_self_billed_numbering/i)
|
|
})
|
|
|
|
it('still rejects a NON-self-billed sent invoice with a NULL number', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
|
|
await expect(
|
|
insertSelfBilled({
|
|
userId,
|
|
companyId,
|
|
isSelfBilled: false,
|
|
externalNumber: null,
|
|
invoiceNumber: null,
|
|
status: 'sent',
|
|
}),
|
|
).rejects.toThrow(/invoices_sent_requires_number/i)
|
|
})
|
|
})
|