Files
accounted/tests/pg/self-billing-invoice.pg.test.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

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)
})
})