* 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
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getPool } from './setup'
|
|
import { seedCompany } from './fixtures'
|
|
|
|
async function insertInvoice(input: {
|
|
userId: string
|
|
companyId: string
|
|
number: string
|
|
creditedInvoiceId?: string
|
|
creationComplete?: boolean
|
|
}): Promise<string> {
|
|
const id = randomUUID()
|
|
await getPool().query(
|
|
`INSERT INTO public.invoices
|
|
(id, user_id, company_id, invoice_number, invoice_date, due_date,
|
|
credited_invoice_id, creation_complete)
|
|
VALUES ($1, $2, $3, $4, '2026-07-15', '2026-07-15', $5, $6)`,
|
|
[
|
|
id,
|
|
input.userId,
|
|
input.companyId,
|
|
input.number,
|
|
input.creditedInvoiceId ?? null,
|
|
input.creationComplete ?? true,
|
|
],
|
|
)
|
|
return id
|
|
}
|
|
|
|
describe('credit note creation and posting guards', () => {
|
|
it('allows only one credit-note relationship per original and company', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
const originalId = await insertInvoice({ userId, companyId, number: `F-${randomUUID()}` })
|
|
|
|
await insertInvoice({
|
|
userId,
|
|
companyId,
|
|
number: `KR-${randomUUID()}`,
|
|
creditedInvoiceId: originalId,
|
|
})
|
|
|
|
await expect(
|
|
insertInvoice({
|
|
userId,
|
|
companyId,
|
|
number: `KR-${randomUUID()}`,
|
|
creditedInvoiceId: originalId,
|
|
}),
|
|
).rejects.toThrow(/duplicate|unique/i)
|
|
})
|
|
|
|
it('persists the completion marker and installs a unique posted-source guard', async () => {
|
|
const { userId, companyId } = await seedCompany()
|
|
const invoiceId = await insertInvoice({
|
|
userId,
|
|
companyId,
|
|
number: `KR-${randomUUID()}`,
|
|
creationComplete: false,
|
|
})
|
|
|
|
const invoice = await getPool().query<{ creation_complete: boolean }>(
|
|
`SELECT creation_complete FROM public.invoices WHERE id = $1`,
|
|
[invoiceId],
|
|
)
|
|
expect(invoice.rows[0].creation_complete).toBe(false)
|
|
|
|
const index = await getPool().query<{ indexdef: string }>(
|
|
`SELECT indexdef FROM pg_indexes
|
|
WHERE schemaname = 'public' AND indexname = 'uq_posted_credit_note_journal_source'`,
|
|
)
|
|
expect(index.rows).toHaveLength(1)
|
|
expect(index.rows[0].indexdef).toMatch(/UNIQUE/)
|
|
expect(index.rows[0].indexdef).toMatch(/source_type.*credit_note/)
|
|
expect(index.rows[0].indexdef).toMatch(/status.*posted/)
|
|
})
|
|
})
|