* 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
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getPool } from '@/tests/pg/setup'
|
|
import { insertAuthUser, insertCompany, insertCompanyMember } from '@/tests/pg/fixtures'
|
|
|
|
async function seedCustomerInvoicePair(): Promise<{
|
|
originalInvoiceId: string
|
|
creditNoteId: string
|
|
}> {
|
|
const userId = await insertAuthUser()
|
|
const companyId = await insertCompany({ createdBy: userId })
|
|
await insertCompanyMember({ companyId, userId })
|
|
|
|
const customerId = randomUUID()
|
|
const originalInvoiceId = randomUUID()
|
|
const creditNoteId = randomUUID()
|
|
|
|
await getPool().query(
|
|
`INSERT INTO public.customers (id, user_id, company_id, name, customer_type)
|
|
VALUES ($1, $2, $3, 'Test Kund AB', 'swedish_business')`,
|
|
[customerId, userId, companyId],
|
|
)
|
|
await getPool().query(
|
|
`INSERT INTO public.invoices
|
|
(id, user_id, company_id, customer_id, invoice_number, invoice_date, due_date,
|
|
currency, subtotal, vat_amount, total, vat_treatment, vat_rate, status,
|
|
paid_amount, remaining_amount)
|
|
VALUES ($1, $2, $3, $4, $5, '2026-07-01', '2026-07-31', 'SEK',
|
|
1000, 0, 1000, 'standard_25', 25, 'sent', 0, 1000)`,
|
|
[originalInvoiceId, userId, companyId, customerId, `F-${originalInvoiceId.slice(0, 8)}`],
|
|
)
|
|
await getPool().query(
|
|
`INSERT INTO public.invoices
|
|
(id, user_id, company_id, customer_id, invoice_number, invoice_date, due_date,
|
|
currency, subtotal, vat_amount, total, vat_treatment, vat_rate, status,
|
|
paid_amount, remaining_amount, credited_invoice_id)
|
|
VALUES ($1, $2, $3, $4, $5, '2026-07-01', '2026-07-31', 'SEK',
|
|
-1000, 0, -1000, 'standard_25', 25, 'sent', 0, -1000, $6)`,
|
|
[creditNoteId, userId, companyId, customerId, `KR-${creditNoteId.slice(0, 8)}`, originalInvoiceId],
|
|
)
|
|
|
|
return { originalInvoiceId, creditNoteId }
|
|
}
|
|
|
|
describe('invoices_credit_note_not_paid constraint', () => {
|
|
it.each(['paid', 'partially_paid'])('rejects credit-note status %s', async (status) => {
|
|
const { creditNoteId } = await seedCustomerInvoicePair()
|
|
|
|
await expect(
|
|
getPool().query(`UPDATE public.invoices SET status = $1 WHERE id = $2`, [status, creditNoteId]),
|
|
).rejects.toMatchObject({ code: '23514' })
|
|
})
|
|
|
|
it('allows an ordinary customer invoice to be marked paid', async () => {
|
|
const { originalInvoiceId } = await seedCustomerInvoicePair()
|
|
|
|
await expect(
|
|
getPool().query(`UPDATE public.invoices SET status = 'paid' WHERE id = $1`, [originalInvoiceId]),
|
|
).resolves.toMatchObject({ rowCount: 1 })
|
|
})
|
|
})
|