6f4573f380
- Add currency-utils module for SEK conversion with exchange rates - Refactor createJournalEntry to use draft+commit flow preventing voucher number gaps (BFL 5 kap. 7§) - Add foreign currency support to invoice entries with per-line SEK conversion - Centralize category-to-account mapping into single source of truth - Refactor invoice inbox to use shared document analyzer with document type classification (receipt, supplier invoice, government letter) - Update mapping engine, supplier invoice entries, and transaction entries - Fix report component rendering issues - Add new validation schemas and tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1985 lines
61 KiB
TypeScript
1985 lines
61 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
// Enums
|
|
EntityTypeSchema,
|
|
CustomerTypeSchema,
|
|
SupplierTypeSchema,
|
|
InvoiceDocumentTypeSchema,
|
|
VatTreatmentSchema,
|
|
AccountingMethodSchema,
|
|
CurrencySchema,
|
|
TransactionCategorySchema,
|
|
JournalEntrySourceTypeSchema,
|
|
AccountTypeSchema,
|
|
NormalBalanceSchema,
|
|
MappingRuleTypeSchema,
|
|
RiskLevelSchema,
|
|
DeadlineTypeSchema,
|
|
DeadlinePrioritySchema,
|
|
TaxDeadlineTypeSchema,
|
|
MomsPeriodSchema,
|
|
DocumentUploadSourceSchema,
|
|
// Invoice schemas
|
|
CreateInvoiceItemSchema,
|
|
CreateInvoiceSchema,
|
|
CreateCreditNoteSchema,
|
|
MarkInvoicePaidSchema,
|
|
// Customer schemas
|
|
CreateCustomerSchema,
|
|
// Supplier schemas
|
|
CreateSupplierSchema,
|
|
// Supplier invoice schemas
|
|
CreateSupplierInvoiceItemSchema,
|
|
CreateSupplierInvoiceSchema,
|
|
MarkSupplierInvoicePaidSchema,
|
|
// Journal entry schemas
|
|
CreateJournalEntryLineSchema,
|
|
CreateJournalEntrySchema,
|
|
// Transaction schemas
|
|
CategorizeTransactionSchema,
|
|
BookTransactionSchema,
|
|
MatchInvoiceSchema,
|
|
MatchSupplierInvoiceSchema,
|
|
// Settings schemas
|
|
UpdateSettingsSchema,
|
|
// Fiscal period schemas
|
|
CreateFiscalPeriodSchema,
|
|
// Mapping rule schemas
|
|
CreateMappingRuleSchema,
|
|
// Deadline schemas
|
|
CreateDeadlineSchema,
|
|
// Account schemas
|
|
CreateAccountSchema,
|
|
UpdateAccountSchema,
|
|
// Reconciliation schemas
|
|
BankLinkSchema,
|
|
BankUnlinkSchema,
|
|
RunReconciliationSchema,
|
|
// Update schemas
|
|
UpdateCustomerSchema,
|
|
UpdateSupplierSchema,
|
|
UpdateSupplierInvoiceSchema,
|
|
// Correct/evaluate schemas
|
|
CorrectJournalEntrySchema,
|
|
EvaluateMappingRulesSchema,
|
|
// Report query schemas
|
|
VatDeclarationQuerySchema,
|
|
PaginationQuerySchema,
|
|
} from '../schemas'
|
|
|
|
// ============================================================
|
|
// Helpers — minimal valid objects for composition
|
|
// ============================================================
|
|
|
|
const validUuid = '550e8400-e29b-41d4-a716-446655440000'
|
|
|
|
function validInvoiceItem(overrides = {}) {
|
|
return { description: 'Consulting', quantity: 10, unit: 'tim', unit_price: 1000, ...overrides }
|
|
}
|
|
|
|
function validInvoice(overrides = {}) {
|
|
return {
|
|
customer_id: validUuid,
|
|
invoice_date: '2025-03-15',
|
|
due_date: '2025-04-14',
|
|
currency: 'SEK' as const,
|
|
items: [validInvoiceItem()],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function validCustomer(overrides = {}) {
|
|
return { name: 'Acme AB', customer_type: 'swedish_business' as const, ...overrides }
|
|
}
|
|
|
|
function validSupplier(overrides = {}) {
|
|
return { name: 'Leverantör AB', supplier_type: 'swedish_business' as const, ...overrides }
|
|
}
|
|
|
|
function validSupplierInvoiceItem(overrides = {}) {
|
|
return { description: 'Material', amount: 5000, account_number: '4010', ...overrides }
|
|
}
|
|
|
|
function validSupplierInvoice(overrides = {}) {
|
|
return {
|
|
supplier_id: validUuid,
|
|
supplier_invoice_number: 'F-2025-001',
|
|
invoice_date: '2025-03-01',
|
|
due_date: '2025-03-31',
|
|
items: [validSupplierInvoiceItem()],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function validJournalEntryLine(overrides = {}) {
|
|
return { account_number: '1930', debit_amount: 1000, credit_amount: 0, ...overrides }
|
|
}
|
|
|
|
function validJournalEntry(overrides = {}) {
|
|
return {
|
|
fiscal_period_id: validUuid,
|
|
entry_date: '2025-03-15',
|
|
description: 'Bank deposit',
|
|
lines: [
|
|
validJournalEntryLine({ account_number: '1930', debit_amount: 1000, credit_amount: 0 }),
|
|
validJournalEntryLine({ account_number: '3001', debit_amount: 0, credit_amount: 1000 }),
|
|
],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Enum schema tests
|
|
// ============================================================
|
|
|
|
describe('Enum schemas', () => {
|
|
it('EntityTypeSchema accepts valid values', () => {
|
|
expect(EntityTypeSchema.safeParse('enskild_firma').success).toBe(true)
|
|
expect(EntityTypeSchema.safeParse('aktiebolag').success).toBe(true)
|
|
})
|
|
|
|
it('EntityTypeSchema rejects invalid values', () => {
|
|
expect(EntityTypeSchema.safeParse('llc').success).toBe(false)
|
|
expect(EntityTypeSchema.safeParse('').success).toBe(false)
|
|
expect(EntityTypeSchema.safeParse(123).success).toBe(false)
|
|
})
|
|
|
|
it('CustomerTypeSchema accepts all 4 types', () => {
|
|
for (const val of ['individual', 'swedish_business', 'eu_business', 'non_eu_business']) {
|
|
expect(CustomerTypeSchema.safeParse(val).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('SupplierTypeSchema accepts 3 types', () => {
|
|
for (const val of ['swedish_business', 'eu_business', 'non_eu_business']) {
|
|
expect(SupplierTypeSchema.safeParse(val).success).toBe(true)
|
|
}
|
|
// individual is not a valid supplier type
|
|
expect(SupplierTypeSchema.safeParse('individual').success).toBe(false)
|
|
})
|
|
|
|
it('VatTreatmentSchema accepts all 6 treatments', () => {
|
|
const treatments = ['standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt']
|
|
for (const val of treatments) {
|
|
expect(VatTreatmentSchema.safeParse(val).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('CurrencySchema accepts supported currencies', () => {
|
|
for (const c of ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']) {
|
|
expect(CurrencySchema.safeParse(c).success).toBe(true)
|
|
}
|
|
expect(CurrencySchema.safeParse('JPY').success).toBe(false)
|
|
})
|
|
|
|
it('TransactionCategorySchema accepts all 16 categories', () => {
|
|
const categories = [
|
|
'income_services', 'income_products', 'income_other',
|
|
'expense_equipment', 'expense_software', 'expense_travel',
|
|
'expense_office', 'expense_marketing', 'expense_professional_services',
|
|
'expense_education', 'expense_bank_fees', 'expense_card_fees',
|
|
'expense_currency_exchange', 'expense_other',
|
|
'private', 'uncategorized',
|
|
]
|
|
for (const c of categories) {
|
|
expect(TransactionCategorySchema.safeParse(c).success).toBe(true)
|
|
}
|
|
expect(TransactionCategorySchema.safeParse('unknown').success).toBe(false)
|
|
})
|
|
|
|
it('JournalEntrySourceTypeSchema accepts all source types', () => {
|
|
const sources = [
|
|
'manual', 'bank_transaction', 'invoice_created', 'invoice_paid',
|
|
'invoice_cash_payment', 'credit_note', 'salary_payment',
|
|
'opening_balance', 'year_end', 'storno', 'correction',
|
|
'import', 'system', 'supplier_invoice_registered',
|
|
'supplier_invoice_paid', 'supplier_invoice_cash_payment', 'supplier_credit_note',
|
|
]
|
|
for (const s of sources) {
|
|
expect(JournalEntrySourceTypeSchema.safeParse(s).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('AccountTypeSchema covers all account classes', () => {
|
|
for (const t of ['asset', 'equity', 'liability', 'revenue', 'expense']) {
|
|
expect(AccountTypeSchema.safeParse(t).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('RiskLevelSchema accepts all risk levels', () => {
|
|
for (const r of ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']) {
|
|
expect(RiskLevelSchema.safeParse(r).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('InvoiceDocumentTypeSchema accepts all document types', () => {
|
|
for (const t of ['invoice', 'proforma', 'delivery_note']) {
|
|
expect(InvoiceDocumentTypeSchema.safeParse(t).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('AccountingMethodSchema accepts accrual and cash', () => {
|
|
expect(AccountingMethodSchema.safeParse('accrual').success).toBe(true)
|
|
expect(AccountingMethodSchema.safeParse('cash').success).toBe(true)
|
|
expect(AccountingMethodSchema.safeParse('hybrid').success).toBe(false)
|
|
})
|
|
|
|
it('MomsPeriodSchema accepts reporting periods', () => {
|
|
for (const p of ['monthly', 'quarterly', 'yearly']) {
|
|
expect(MomsPeriodSchema.safeParse(p).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('DeadlineTypeSchema and DeadlinePrioritySchema', () => {
|
|
for (const t of ['delivery', 'invoicing', 'report', 'tax', 'other']) {
|
|
expect(DeadlineTypeSchema.safeParse(t).success).toBe(true)
|
|
}
|
|
for (const p of ['critical', 'important', 'normal']) {
|
|
expect(DeadlinePrioritySchema.safeParse(p).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('TaxDeadlineTypeSchema accepts all Swedish tax deadlines', () => {
|
|
const types = [
|
|
'moms_monthly', 'moms_quarterly', 'moms_yearly', 'f_skatt',
|
|
'arbetsgivardeklaration', 'inkomstdeklaration_ef', 'inkomstdeklaration_ab',
|
|
'arsredovisning', 'periodisk_sammanstallning', 'bokslut',
|
|
]
|
|
for (const t of types) {
|
|
expect(TaxDeadlineTypeSchema.safeParse(t).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('NormalBalanceSchema and MappingRuleTypeSchema', () => {
|
|
for (const b of ['debit', 'credit']) {
|
|
expect(NormalBalanceSchema.safeParse(b).success).toBe(true)
|
|
}
|
|
for (const t of ['mcc_code', 'merchant_name', 'description_pattern', 'amount_threshold', 'combined']) {
|
|
expect(MappingRuleTypeSchema.safeParse(t).success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('DocumentUploadSourceSchema accepts all sources', () => {
|
|
for (const s of ['camera', 'file_upload', 'email', 'e_invoice', 'scan', 'api', 'system']) {
|
|
expect(DocumentUploadSourceSchema.safeParse(s).success).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Invoice schemas
|
|
// ============================================================
|
|
|
|
describe('CreateInvoiceSchema', () => {
|
|
it('accepts a valid invoice', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts invoice with optional fields', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
document_type: 'proforma',
|
|
your_reference: 'John Doe',
|
|
our_reference: 'Jane Doe',
|
|
notes: 'Net 30',
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts invoice with per-line VAT rates', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [
|
|
validInvoiceItem({ vat_rate: 0.25 }),
|
|
validInvoiceItem({ description: 'Food', vat_rate: 0.12 }),
|
|
validInvoiceItem({ description: 'Books', vat_rate: 0.06 }),
|
|
],
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing customer_id', () => {
|
|
const { customer_id: _, ...rest } = validInvoice()
|
|
const result = CreateInvoiceSchema.safeParse(rest)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid customer_id (not UUID)', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ customer_id: 'not-a-uuid' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid date format', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ invoice_date: '15/03/2025' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid currency', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ currency: 'JPY' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty items array', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ items: [] }))
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const itemsError = result.error.issues.find(i => i.path.includes('items'))
|
|
expect(itemsError?.message).toContain('At least one item')
|
|
}
|
|
})
|
|
|
|
it('rejects item with empty description', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ description: '' })],
|
|
}))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects item with zero quantity', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ quantity: 0 })],
|
|
}))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects item with negative quantity', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ quantity: -5 })],
|
|
}))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects vat_rate > 100', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ vat_rate: 101 })],
|
|
}))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts vat_rate of 25 (standard Swedish VAT)', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ vat_rate: 25 })],
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts vat_rate of 0 (export/exempt)', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ vat_rate: 0 })],
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid document_type', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ document_type: 'receipt' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('allows negative unit_price (for discounts)', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ unit_price: -100 })],
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('CreateInvoiceItemSchema', () => {
|
|
it('accepts valid item with all fields', () => {
|
|
const result = CreateInvoiceItemSchema.safeParse(validInvoiceItem({ vat_rate: 0.25 }))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts item without vat_rate (uses invoice default)', () => {
|
|
const result = CreateInvoiceItemSchema.safeParse(validInvoiceItem())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects non-numeric quantity', () => {
|
|
const result = CreateInvoiceItemSchema.safeParse(validInvoiceItem({ quantity: 'ten' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('CreateCreditNoteSchema', () => {
|
|
it('accepts valid credit note reference', () => {
|
|
const result = CreateCreditNoteSchema.safeParse({ credited_invoice_id: validUuid })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts credit note with reason', () => {
|
|
const result = CreateCreditNoteSchema.safeParse({
|
|
credited_invoice_id: validUuid,
|
|
reason: 'Duplicate billing',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing credited_invoice_id', () => {
|
|
const result = CreateCreditNoteSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-UUID credited_invoice_id', () => {
|
|
const result = CreateCreditNoteSchema.safeParse({ credited_invoice_id: 'INV-001' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('MarkInvoicePaidSchema', () => {
|
|
it('accepts empty object (all fields optional)', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts payment_date', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({ payment_date: '2024-07-15' })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts exchange_rate_difference (positive gain)', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({ exchange_rate_difference: 200 })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts exchange_rate_difference (negative loss)', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({ exchange_rate_difference: -300 })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts all fields together', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({
|
|
payment_date: '2024-07-15',
|
|
exchange_rate_difference: 150.50,
|
|
notes: 'Paid via Wise',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid payment_date format', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({ payment_date: '15/07/2024' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-number exchange_rate_difference', () => {
|
|
const result = MarkInvoicePaidSchema.safeParse({ exchange_rate_difference: 'big gain' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Customer schemas
|
|
// ============================================================
|
|
|
|
describe('CreateCustomerSchema', () => {
|
|
it('accepts valid customer with minimal fields', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts customer with all optional fields', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({
|
|
email: 'billing@acme.se',
|
|
phone: '+46701234567',
|
|
address_line1: 'Storgatan 1',
|
|
address_line2: 'Box 123',
|
|
postal_code: '111 22',
|
|
city: 'Stockholm',
|
|
country: 'Sweden',
|
|
org_number: '556123-4567',
|
|
vat_number: 'SE556123456701',
|
|
default_payment_terms: 30,
|
|
notes: 'Key account',
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects empty name', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ name: '' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing name', () => {
|
|
const { name: _, ...rest } = validCustomer()
|
|
const result = CreateCustomerSchema.safeParse(rest)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid customer_type', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ customer_type: 'government' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid email format', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ email: 'not-an-email' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts valid email', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ email: 'test@example.com' }))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects negative payment terms', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ default_payment_terms: -10 }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects zero payment terms', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ default_payment_terms: 0 }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-integer payment terms', () => {
|
|
const result = CreateCustomerSchema.safeParse(validCustomer({ default_payment_terms: 30.5 }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Supplier schemas
|
|
// ============================================================
|
|
|
|
describe('CreateSupplierSchema', () => {
|
|
it('accepts valid supplier with minimal fields', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts supplier with payment details', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier({
|
|
bankgiro: '123-4567',
|
|
plusgiro: '12345-6',
|
|
iban: 'SE1234567890123456789',
|
|
bic: 'ESSESESS',
|
|
default_expense_account: '4010',
|
|
default_payment_terms: 30,
|
|
default_currency: 'SEK',
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects empty name', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier({ name: '' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid supplier_type', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier({ supplier_type: 'individual' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid expense account format', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier({ default_expense_account: '40' }))
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts valid 4-digit expense account', () => {
|
|
const result = CreateSupplierSchema.safeParse(validSupplier({ default_expense_account: '6200' }))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Supplier invoice schemas
|
|
// ============================================================
|
|
|
|
describe('CreateSupplierInvoiceSchema', () => {
|
|
it('accepts valid supplier invoice', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(validSupplierInvoice())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts invoice with all optional fields', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(validSupplierInvoice({
|
|
delivery_date: '2025-03-15',
|
|
currency: 'EUR',
|
|
exchange_rate: 11.35,
|
|
vat_treatment: 'reverse_charge',
|
|
reverse_charge: true,
|
|
payment_reference: 'OCR-123456',
|
|
notes: 'Quarterly supply',
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing supplier_id', () => {
|
|
const { supplier_id: _, ...rest } = validSupplierInvoice()
|
|
const result = CreateSupplierInvoiceSchema.safeParse(rest)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty supplier invoice number', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({ supplier_invoice_number: '' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty items array', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({ items: [] })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects item with invalid account number', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({
|
|
items: [validSupplierInvoiceItem({ account_number: 'abc' })],
|
|
})
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects zero exchange rate', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({ exchange_rate: 0 })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative exchange rate', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({ exchange_rate: -1.5 })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts item with legacy quantity/unit_price fields', () => {
|
|
const result = CreateSupplierInvoiceSchema.safeParse(
|
|
validSupplierInvoice({
|
|
items: [validSupplierInvoiceItem({ quantity: 10, unit: 'st', unit_price: 500 })],
|
|
})
|
|
)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('CreateSupplierInvoiceItemSchema', () => {
|
|
it('accepts valid item', () => {
|
|
const result = CreateSupplierInvoiceItemSchema.safeParse(validSupplierInvoiceItem())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects item with 3-digit account number', () => {
|
|
const result = CreateSupplierInvoiceItemSchema.safeParse(
|
|
validSupplierInvoiceItem({ account_number: '401' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects item with 5-digit account number', () => {
|
|
const result = CreateSupplierInvoiceItemSchema.safeParse(
|
|
validSupplierInvoiceItem({ account_number: '40100' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts vat_rate within valid range', () => {
|
|
for (const rate of [0, 0.06, 0.12, 0.25]) {
|
|
const result = CreateSupplierInvoiceItemSchema.safeParse(
|
|
validSupplierInvoiceItem({ vat_rate: rate })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('MarkSupplierInvoicePaidSchema', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts full payment details', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({
|
|
amount: 5000,
|
|
payment_date: '2025-03-31',
|
|
exchange_rate_difference: -12.50,
|
|
notes: 'Paid via bank transfer',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects zero amount', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({ amount: 0 })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative amount', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({ amount: -100 })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid payment_date format', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({ payment_date: '2025/03/31' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('allows negative exchange_rate_difference (loss)', () => {
|
|
const result = MarkSupplierInvoicePaidSchema.safeParse({ exchange_rate_difference: -50.25 })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Journal entry schemas
|
|
// ============================================================
|
|
|
|
describe('CreateJournalEntrySchema', () => {
|
|
it('accepts valid balanced entry', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(validJournalEntry())
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts entry with optional source_type', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ source_type: 'manual' })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts entry with all source types', () => {
|
|
const sourceTypes = [
|
|
'manual', 'bank_transaction', 'invoice_created', 'invoice_paid',
|
|
'storno', 'correction', 'system',
|
|
]
|
|
for (const source_type of sourceTypes) {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ source_type })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('rejects entry with only one line (not double-entry)', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ lines: [validJournalEntryLine()] })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const linesError = result.error.issues.find(i => i.path.includes('lines'))
|
|
expect(linesError?.message).toContain('two lines')
|
|
}
|
|
})
|
|
|
|
it('rejects entry with empty lines', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ lines: [] })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing description', () => {
|
|
const { description: _, ...rest } = validJournalEntry()
|
|
const result = CreateJournalEntrySchema.safeParse(rest)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty description', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ description: '' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid fiscal_period_id', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ fiscal_period_id: 'not-uuid' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid entry_date format', () => {
|
|
const result = CreateJournalEntrySchema.safeParse(
|
|
validJournalEntry({ entry_date: '2025-3-15' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('CreateJournalEntryLineSchema', () => {
|
|
it('accepts valid debit line', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ debit_amount: 1000, credit_amount: 0 })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts valid credit line', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ debit_amount: 0, credit_amount: 1000 })
|
|
)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts line with currency info', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse({
|
|
account_number: '1930',
|
|
debit_amount: 11350,
|
|
credit_amount: 0,
|
|
currency: 'EUR',
|
|
amount_in_currency: 1000,
|
|
exchange_rate: 11.35,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts line with cost center and project', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse({
|
|
...validJournalEntryLine(),
|
|
cost_center: 'CC-100',
|
|
project: 'PROJ-2025-01',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid account number', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ account_number: '19' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects account number with letters', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ account_number: '193a' })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative debit_amount', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ debit_amount: -100 })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative credit_amount', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ credit_amount: -100 })
|
|
)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('defaults debit_amount and credit_amount to 0', () => {
|
|
const result = CreateJournalEntryLineSchema.safeParse({ account_number: '1930' })
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(result.data.debit_amount).toBe(0)
|
|
expect(result.data.credit_amount).toBe(0)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Transaction schemas
|
|
// ============================================================
|
|
|
|
describe('CategorizeTransactionSchema', () => {
|
|
it('accepts minimal categorization (private)', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({ is_business: false })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts business categorization with details', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({
|
|
is_business: true,
|
|
category: 'expense_office',
|
|
vat_treatment: 'standard_25',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts account override', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({
|
|
is_business: true,
|
|
category: 'expense_equipment',
|
|
account_override: '1250',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing is_business', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({ category: 'private' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-boolean is_business', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({ is_business: 'yes' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid category', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({
|
|
is_business: true,
|
|
category: 'food',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid account_override format', () => {
|
|
const result = CategorizeTransactionSchema.safeParse({
|
|
is_business: true,
|
|
account_override: '12',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('BookTransactionSchema', () => {
|
|
it('accepts valid booking', () => {
|
|
const result = BookTransactionSchema.safeParse({
|
|
fiscal_period_id: validUuid,
|
|
entry_date: '2025-03-15',
|
|
description: 'Office supplies',
|
|
lines: [
|
|
{ account_number: '6100', debit_amount: 800, credit_amount: 0 },
|
|
{ account_number: '2641', debit_amount: 200, credit_amount: 0 },
|
|
{ account_number: '1930', debit_amount: 0, credit_amount: 1000 },
|
|
],
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects empty description', () => {
|
|
const result = BookTransactionSchema.safeParse({
|
|
fiscal_period_id: validUuid,
|
|
entry_date: '2025-03-15',
|
|
description: '',
|
|
lines: [validJournalEntryLine()],
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty lines', () => {
|
|
const result = BookTransactionSchema.safeParse({
|
|
fiscal_period_id: validUuid,
|
|
entry_date: '2025-03-15',
|
|
description: 'Test',
|
|
lines: [],
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('MatchInvoiceSchema', () => {
|
|
it('accepts valid invoice_id', () => {
|
|
const result = MatchInvoiceSchema.safeParse({ invoice_id: validUuid })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing invoice_id', () => {
|
|
const result = MatchInvoiceSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-UUID invoice_id', () => {
|
|
const result = MatchInvoiceSchema.safeParse({ invoice_id: 'INV-001' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('MatchSupplierInvoiceSchema', () => {
|
|
it('accepts valid supplier_invoice_id', () => {
|
|
const result = MatchSupplierInvoiceSchema.safeParse({ supplier_invoice_id: validUuid })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing supplier_invoice_id', () => {
|
|
const result = MatchSupplierInvoiceSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Settings schemas
|
|
// ============================================================
|
|
|
|
describe('UpdateSettingsSchema', () => {
|
|
it('accepts empty update (no changes)', () => {
|
|
const result = UpdateSettingsSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts partial update', () => {
|
|
const result = UpdateSettingsSchema.safeParse({
|
|
company_name: 'My AB',
|
|
vat_registered: true,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts full update', () => {
|
|
const result = UpdateSettingsSchema.safeParse({
|
|
entity_type: 'aktiebolag',
|
|
company_name: 'Tech AB',
|
|
org_number: '556123-4567',
|
|
f_skatt: true,
|
|
vat_registered: true,
|
|
vat_number: 'SE556123456701',
|
|
moms_period: 'quarterly',
|
|
fiscal_year_start_month: 7,
|
|
accounting_method: 'accrual',
|
|
invoice_default_days: 30,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('enforces BFL 3 kap: enskild firma must start in January', () => {
|
|
const result = UpdateSettingsSchema.safeParse({
|
|
entity_type: 'enskild_firma',
|
|
fiscal_year_start_month: 7,
|
|
})
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const bflError = result.error.issues.find(i =>
|
|
i.path.includes('fiscal_year_start_month')
|
|
)
|
|
expect(bflError?.message).toContain('BFL')
|
|
}
|
|
})
|
|
|
|
it('allows enskild firma with January start', () => {
|
|
const result = UpdateSettingsSchema.safeParse({
|
|
entity_type: 'enskild_firma',
|
|
fiscal_year_start_month: 1,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('allows aktiebolag with any start month', () => {
|
|
for (let month = 1; month <= 12; month++) {
|
|
const result = UpdateSettingsSchema.safeParse({
|
|
entity_type: 'aktiebolag',
|
|
fiscal_year_start_month: month,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('rejects fiscal_year_start_month out of range', () => {
|
|
expect(UpdateSettingsSchema.safeParse({ fiscal_year_start_month: 0 }).success).toBe(false)
|
|
expect(UpdateSettingsSchema.safeParse({ fiscal_year_start_month: 13 }).success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid accounting_method', () => {
|
|
const result = UpdateSettingsSchema.safeParse({ accounting_method: 'hybrid' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts null moms_period (unregistered)', () => {
|
|
const result = UpdateSettingsSchema.safeParse({ moms_period: null })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid email', () => {
|
|
const result = UpdateSettingsSchema.safeParse({ email: 'not-email' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-integer invoice_default_days', () => {
|
|
const result = UpdateSettingsSchema.safeParse({ invoice_default_days: 30.5 })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Fiscal period schemas
|
|
// ============================================================
|
|
|
|
describe('CreateFiscalPeriodSchema', () => {
|
|
it('accepts valid period', () => {
|
|
const result = CreateFiscalPeriodSchema.safeParse({
|
|
name: 'FY 2025',
|
|
period_start: '2025-01-01',
|
|
period_end: '2025-12-31',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects end before start', () => {
|
|
const result = CreateFiscalPeriodSchema.safeParse({
|
|
name: 'FY 2025',
|
|
period_start: '2025-12-31',
|
|
period_end: '2025-01-01',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
expect(result.error.issues[0].message).toContain('before')
|
|
}
|
|
})
|
|
|
|
it('rejects same start and end date', () => {
|
|
const result = CreateFiscalPeriodSchema.safeParse({
|
|
name: 'FY 2025',
|
|
period_start: '2025-01-01',
|
|
period_end: '2025-01-01',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty name', () => {
|
|
const result = CreateFiscalPeriodSchema.safeParse({
|
|
name: '',
|
|
period_start: '2025-01-01',
|
|
period_end: '2025-12-31',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid date format', () => {
|
|
const result = CreateFiscalPeriodSchema.safeParse({
|
|
name: 'FY 2025',
|
|
period_start: 'Jan 1, 2025',
|
|
period_end: '2025-12-31',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Mapping rule schemas
|
|
// ============================================================
|
|
|
|
describe('CreateMappingRuleSchema', () => {
|
|
it('accepts valid rule', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Office rent',
|
|
rule_type: 'merchant_name',
|
|
merchant_pattern: 'Vasakronan',
|
|
debit_account: '5010',
|
|
credit_account: '1930',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts rule with all optional fields', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Restaurant meals',
|
|
rule_type: 'mcc_code',
|
|
priority: 5,
|
|
mcc_codes: ['5812', '5811'],
|
|
debit_account: '6071',
|
|
credit_account: '1930',
|
|
vat_treatment: 'reduced_12',
|
|
risk_level: 'LOW',
|
|
default_private: false,
|
|
requires_review: true,
|
|
confidence_score: 0.85,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing debit_account', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Test',
|
|
rule_type: 'merchant_name',
|
|
credit_account: '1930',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid account format', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Test',
|
|
rule_type: 'merchant_name',
|
|
debit_account: '50',
|
|
credit_account: '1930',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects confidence_score > 1', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Test',
|
|
rule_type: 'merchant_name',
|
|
debit_account: '5010',
|
|
credit_account: '1930',
|
|
confidence_score: 1.5,
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative confidence_score', () => {
|
|
const result = CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Test',
|
|
rule_type: 'merchant_name',
|
|
debit_account: '5010',
|
|
credit_account: '1930',
|
|
confidence_score: -0.1,
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Deadline schemas
|
|
// ============================================================
|
|
|
|
describe('CreateDeadlineSchema', () => {
|
|
it('accepts valid deadline', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
title: 'Momsdeklaration Q1',
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts deadline with all optional fields', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
title: 'Momsdeklaration Q1',
|
|
due_date: '2025-05-12',
|
|
due_time: '23:59',
|
|
deadline_type: 'tax',
|
|
priority: 'critical',
|
|
customer_id: validUuid,
|
|
notes: 'Submit via Skatteverket',
|
|
tax_deadline_type: 'moms_quarterly',
|
|
tax_period: '2025-Q1',
|
|
source: 'system',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing title', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty title', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
title: '',
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid due_time format', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
title: 'Test',
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
due_time: '25:00',
|
|
})
|
|
// Note: regex accepts 25:00 — business logic validates actual time values
|
|
// This test documents the current behavior
|
|
const parsed = CreateDeadlineSchema.safeParse({
|
|
title: 'Test',
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
due_time: 'noon',
|
|
})
|
|
expect(parsed.success).toBe(false)
|
|
})
|
|
|
|
it('accepts due_time with seconds', () => {
|
|
const result = CreateDeadlineSchema.safeParse({
|
|
title: 'Test',
|
|
due_date: '2025-05-12',
|
|
deadline_type: 'tax',
|
|
due_time: '23:59:59',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Account schemas
|
|
// ============================================================
|
|
|
|
describe('CreateAccountSchema', () => {
|
|
it('accepts valid BAS account', () => {
|
|
const result = CreateAccountSchema.safeParse({
|
|
account_number: '6200',
|
|
account_name: 'Telefon & internet',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts with optional plan_type and description', () => {
|
|
const result = CreateAccountSchema.safeParse({
|
|
account_number: '1510',
|
|
account_name: 'Kundfordringar',
|
|
account_type: 'asset',
|
|
normal_balance: 'debit',
|
|
plan_type: 'k1',
|
|
description: 'Accounts receivable from customers',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects non-4-digit account number', () => {
|
|
expect(CreateAccountSchema.safeParse({
|
|
account_number: '62',
|
|
account_name: 'Test',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
}).success).toBe(false)
|
|
|
|
expect(CreateAccountSchema.safeParse({
|
|
account_number: '62000',
|
|
account_name: 'Test',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
}).success).toBe(false)
|
|
})
|
|
|
|
it('rejects account number with letters', () => {
|
|
const result = CreateAccountSchema.safeParse({
|
|
account_number: '620A',
|
|
account_name: 'Test',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty account_name', () => {
|
|
const result = CreateAccountSchema.safeParse({
|
|
account_number: '6200',
|
|
account_name: '',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid account_type', () => {
|
|
const result = CreateAccountSchema.safeParse({
|
|
account_number: '6200',
|
|
account_name: 'Test',
|
|
account_type: 'cost',
|
|
normal_balance: 'debit',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Bank reconciliation schemas
|
|
// ============================================================
|
|
|
|
describe('BankLinkSchema', () => {
|
|
it('accepts valid link', () => {
|
|
const result = BankLinkSchema.safeParse({
|
|
transaction_id: validUuid,
|
|
journal_entry_id: validUuid,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing transaction_id', () => {
|
|
const result = BankLinkSchema.safeParse({ journal_entry_id: validUuid })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing journal_entry_id', () => {
|
|
const result = BankLinkSchema.safeParse({ transaction_id: validUuid })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-UUID values', () => {
|
|
const result = BankLinkSchema.safeParse({
|
|
transaction_id: 'txn-123',
|
|
journal_entry_id: 'je-456',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Report query schemas
|
|
// ============================================================
|
|
|
|
describe('VatDeclarationQuerySchema', () => {
|
|
it('accepts valid monthly query', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'monthly',
|
|
year: '2025',
|
|
period: '3',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(result.data.year).toBe(2025)
|
|
expect(result.data.period).toBe(3)
|
|
}
|
|
})
|
|
|
|
it('accepts valid quarterly query', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'quarterly',
|
|
year: '2025',
|
|
period: '2',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('coerces string numbers to numbers', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'yearly',
|
|
year: '2025',
|
|
period: '1',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(typeof result.data.year).toBe('number')
|
|
expect(typeof result.data.period).toBe('number')
|
|
}
|
|
})
|
|
|
|
it('rejects year below 2000', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'monthly',
|
|
year: '1999',
|
|
period: '1',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects year above 2100', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'monthly',
|
|
year: '2101',
|
|
period: '1',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects period below 1', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'monthly',
|
|
year: '2025',
|
|
period: '0',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects period above 12', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'monthly',
|
|
year: '2025',
|
|
period: '13',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid periodType', () => {
|
|
const result = VatDeclarationQuerySchema.safeParse({
|
|
periodType: 'biweekly',
|
|
year: '2025',
|
|
period: '1',
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('PaginationQuerySchema', () => {
|
|
it('accepts valid pagination', () => {
|
|
const result = PaginationQuerySchema.safeParse({ limit: '25', offset: '50' })
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(result.data.limit).toBe(25)
|
|
expect(result.data.offset).toBe(50)
|
|
}
|
|
})
|
|
|
|
it('applies defaults when empty', () => {
|
|
const result = PaginationQuerySchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
if (result.success) {
|
|
expect(result.data.limit).toBe(50)
|
|
expect(result.data.offset).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('rejects limit above 100', () => {
|
|
const result = PaginationQuerySchema.safeParse({ limit: '101' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects limit below 1', () => {
|
|
const result = PaginationQuerySchema.safeParse({ limit: '0' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects negative offset', () => {
|
|
const result = PaginationQuerySchema.safeParse({ offset: '-1' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Update schemas (partial variants)
|
|
// ============================================================
|
|
|
|
describe('UpdateCustomerSchema', () => {
|
|
it('accepts empty update (all fields optional)', () => {
|
|
const result = UpdateCustomerSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts partial update', () => {
|
|
const result = UpdateCustomerSchema.safeParse({ name: 'New Name' })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts full update (same as create)', () => {
|
|
const result = UpdateCustomerSchema.safeParse(validCustomer({
|
|
email: 'new@acme.se',
|
|
phone: '+46701111111',
|
|
}))
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid email in partial update', () => {
|
|
const result = UpdateCustomerSchema.safeParse({ email: 'not-email' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid customer_type in partial update', () => {
|
|
const result = UpdateCustomerSchema.safeParse({ customer_type: 'government' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('UpdateSupplierSchema', () => {
|
|
it('accepts empty update', () => {
|
|
const result = UpdateSupplierSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts partial update', () => {
|
|
const result = UpdateSupplierSchema.safeParse({
|
|
name: 'New Supplier',
|
|
bankgiro: '999-8888',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid expense account format', () => {
|
|
const result = UpdateSupplierSchema.safeParse({ default_expense_account: '40' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid supplier_type', () => {
|
|
const result = UpdateSupplierSchema.safeParse({ supplier_type: 'individual' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('UpdateSupplierInvoiceSchema', () => {
|
|
it('accepts empty update', () => {
|
|
const result = UpdateSupplierInvoiceSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts partial update with dates', () => {
|
|
const result = UpdateSupplierInvoiceSchema.safeParse({
|
|
due_date: '2025-04-30',
|
|
payment_reference: 'OCR-999',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts all fields', () => {
|
|
const result = UpdateSupplierInvoiceSchema.safeParse({
|
|
supplier_invoice_number: 'F-2025-002',
|
|
invoice_date: '2025-03-01',
|
|
due_date: '2025-04-01',
|
|
delivery_date: '2025-03-15',
|
|
payment_reference: 'REF-123',
|
|
notes: 'Updated notes',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid date format', () => {
|
|
const result = UpdateSupplierInvoiceSchema.safeParse({ due_date: '2025/04/30' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty supplier_invoice_number', () => {
|
|
const result = UpdateSupplierInvoiceSchema.safeParse({ supplier_invoice_number: '' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('UpdateAccountSchema', () => {
|
|
it('accepts empty update', () => {
|
|
const result = UpdateAccountSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts partial update', () => {
|
|
const result = UpdateAccountSchema.safeParse({
|
|
account_name: 'Nytt kontonamn',
|
|
is_active: false,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts nullable fields', () => {
|
|
const result = UpdateAccountSchema.safeParse({
|
|
description: null,
|
|
default_vat_code: null,
|
|
sru_code: null,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects empty account_name', () => {
|
|
const result = UpdateAccountSchema.safeParse({ account_name: '' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-boolean is_active', () => {
|
|
const result = UpdateAccountSchema.safeParse({ is_active: 'yes' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Bank reconciliation new schemas
|
|
// ============================================================
|
|
|
|
describe('BankUnlinkSchema', () => {
|
|
it('accepts valid transaction_id', () => {
|
|
const result = BankUnlinkSchema.safeParse({ transaction_id: validUuid })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing transaction_id', () => {
|
|
const result = BankUnlinkSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-UUID transaction_id', () => {
|
|
const result = BankUnlinkSchema.safeParse({ transaction_id: 'txn-123' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('RunReconciliationSchema', () => {
|
|
it('accepts empty object (all optional)', () => {
|
|
const result = RunReconciliationSchema.safeParse({})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts full options', () => {
|
|
const result = RunReconciliationSchema.safeParse({
|
|
date_from: '2025-01-01',
|
|
date_to: '2025-03-31',
|
|
dry_run: true,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts dry_run false', () => {
|
|
const result = RunReconciliationSchema.safeParse({ dry_run: false })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid date_from format', () => {
|
|
const result = RunReconciliationSchema.safeParse({ date_from: '2025/01/01' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects non-boolean dry_run', () => {
|
|
const result = RunReconciliationSchema.safeParse({ dry_run: 'yes' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Correct journal entry schema
|
|
// ============================================================
|
|
|
|
describe('CorrectJournalEntrySchema', () => {
|
|
it('accepts valid correction with balanced lines', () => {
|
|
const result = CorrectJournalEntrySchema.safeParse({
|
|
lines: [
|
|
validJournalEntryLine({ account_number: '6200', debit_amount: 500, credit_amount: 0 }),
|
|
validJournalEntryLine({ account_number: '1930', debit_amount: 0, credit_amount: 500 }),
|
|
],
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects single line (not double-entry)', () => {
|
|
const result = CorrectJournalEntrySchema.safeParse({
|
|
lines: [validJournalEntryLine()],
|
|
})
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const linesError = result.error.issues.find(i => i.path.includes('lines'))
|
|
expect(linesError?.message).toContain('two lines')
|
|
}
|
|
})
|
|
|
|
it('rejects empty lines array', () => {
|
|
const result = CorrectJournalEntrySchema.safeParse({ lines: [] })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing lines', () => {
|
|
const result = CorrectJournalEntrySchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects invalid account number in lines', () => {
|
|
const result = CorrectJournalEntrySchema.safeParse({
|
|
lines: [
|
|
validJournalEntryLine({ account_number: '62' }),
|
|
validJournalEntryLine({ account_number: '1930' }),
|
|
],
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Evaluate mapping rules schema
|
|
// ============================================================
|
|
|
|
describe('EvaluateMappingRulesSchema', () => {
|
|
it('accepts valid transaction_id', () => {
|
|
const result = EvaluateMappingRulesSchema.safeParse({ transaction_id: validUuid })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts raw transaction data with amount', () => {
|
|
const result = EvaluateMappingRulesSchema.safeParse({
|
|
description: 'Office supplies',
|
|
amount: -500,
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts raw data with all fields', () => {
|
|
const result = EvaluateMappingRulesSchema.safeParse({
|
|
description: 'Spotify',
|
|
amount: -129,
|
|
merchant_name: 'Spotify AB',
|
|
mcc_code: '5815',
|
|
date: '2025-03-15',
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects non-UUID transaction_id', () => {
|
|
// First branch fails (invalid UUID), second branch matches only if amount is present
|
|
const result = EvaluateMappingRulesSchema.safeParse({ transaction_id: 'not-uuid' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects empty object (no transaction_id and no amount)', () => {
|
|
const result = EvaluateMappingRulesSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing amount in raw data', () => {
|
|
const result = EvaluateMappingRulesSchema.safeParse({ description: 'Test' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Cross-schema consistency tests
|
|
// ============================================================
|
|
|
|
describe('Cross-schema consistency', () => {
|
|
it('account_number format is enforced identically across schemas', () => {
|
|
// All schemas that accept account_number should use the same 4-digit rule
|
|
const invalidAccounts = ['12', '123', '12345', 'ABCD', '1a3b', '']
|
|
|
|
for (const acct of invalidAccounts) {
|
|
// Journal entry line
|
|
expect(CreateJournalEntryLineSchema.safeParse(
|
|
validJournalEntryLine({ account_number: acct })
|
|
).success).toBe(false)
|
|
|
|
// Supplier invoice item
|
|
expect(CreateSupplierInvoiceItemSchema.safeParse(
|
|
validSupplierInvoiceItem({ account_number: acct })
|
|
).success).toBe(false)
|
|
|
|
// Account creation
|
|
expect(CreateAccountSchema.safeParse({
|
|
account_number: acct,
|
|
account_name: 'Test',
|
|
account_type: 'expense',
|
|
normal_balance: 'debit',
|
|
}).success).toBe(false)
|
|
|
|
// Mapping rule accounts
|
|
expect(CreateMappingRuleSchema.safeParse({
|
|
rule_name: 'Test',
|
|
rule_type: 'merchant_name',
|
|
debit_account: acct,
|
|
credit_account: '1930',
|
|
}).success).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('date format is enforced identically across schemas', () => {
|
|
const invalidDates = ['2025/03/15', '15-03-2025', 'Mar 15 2025', '2025-3-15', '']
|
|
|
|
for (const date of invalidDates) {
|
|
expect(CreateInvoiceSchema.safeParse(
|
|
validInvoice({ invoice_date: date })
|
|
).success).toBe(false)
|
|
|
|
expect(CreateFiscalPeriodSchema.safeParse({
|
|
name: 'Test', period_start: date, period_end: '2025-12-31',
|
|
}).success).toBe(false)
|
|
|
|
expect(CreateDeadlineSchema.safeParse({
|
|
title: 'Test', due_date: date, deadline_type: 'tax',
|
|
}).success).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('UUID format is enforced identically across schemas', () => {
|
|
const invalidUuids = ['not-a-uuid', '123', '', '550e8400-e29b-41d4-a716']
|
|
|
|
for (const id of invalidUuids) {
|
|
expect(CreateInvoiceSchema.safeParse(
|
|
validInvoice({ customer_id: id })
|
|
).success).toBe(false)
|
|
|
|
expect(MatchInvoiceSchema.safeParse({ invoice_id: id }).success).toBe(false)
|
|
|
|
expect(BankLinkSchema.safeParse({
|
|
transaction_id: id, journal_entry_id: validUuid,
|
|
}).success).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Error message quality tests
|
|
// ============================================================
|
|
|
|
describe('Error messages', () => {
|
|
it('provides field path in validation errors', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({
|
|
items: [validInvoiceItem({ description: '' })],
|
|
}))
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const issue = result.error.issues[0]
|
|
expect(issue.path).toContain('items')
|
|
}
|
|
})
|
|
|
|
it('reports all errors, not just the first', () => {
|
|
const result = CreateInvoiceSchema.safeParse({
|
|
// Missing everything
|
|
})
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
// Should report errors for customer_id, invoice_date, due_date, currency, items
|
|
expect(result.error.issues.length).toBeGreaterThanOrEqual(4)
|
|
}
|
|
})
|
|
|
|
it('custom messages are human-readable', () => {
|
|
const result = CreateInvoiceSchema.safeParse(validInvoice({ items: [] }))
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
const msg = result.error.issues[0].message
|
|
expect(msg).toMatch(/item/i)
|
|
}
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Integration with existing fixture factories
|
|
// ============================================================
|
|
|
|
describe('Integration with test helpers', () => {
|
|
// These tests demonstrate that Zod schemas align with the fixture factories
|
|
// from tests/helpers.ts, ensuring schema and test data stay in sync.
|
|
|
|
it('CreateCustomerSchema matches makeCustomer() shape', () => {
|
|
// Simulate the shape produced by makeCustomer()
|
|
const customerData = {
|
|
name: 'Test Customer 1',
|
|
customer_type: 'swedish_business',
|
|
email: 'customer-1@test.com',
|
|
phone: '+46701234567',
|
|
address_line1: 'Testgatan 1',
|
|
postal_code: '111 22',
|
|
city: 'Stockholm',
|
|
country: 'SE',
|
|
default_payment_terms: 30,
|
|
}
|
|
const result = CreateCustomerSchema.safeParse(customerData)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('CreateSupplierSchema matches makeSupplier() shape', () => {
|
|
const supplierData = {
|
|
name: 'Test Supplier 1',
|
|
supplier_type: 'swedish_business',
|
|
email: 'supplier-1@test.com',
|
|
default_expense_account: '4010',
|
|
default_payment_terms: 30,
|
|
default_currency: 'SEK',
|
|
}
|
|
const result = CreateSupplierSchema.safeParse(supplierData)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('CreateJournalEntrySchema validates balanced entries from fixture', () => {
|
|
const entryInput = {
|
|
fiscal_period_id: validUuid,
|
|
entry_date: '2025-01-15',
|
|
description: 'Test entry',
|
|
source_type: 'manual',
|
|
lines: [
|
|
{ account_number: '1930', debit_amount: 10000, credit_amount: 0 },
|
|
{ account_number: '3001', debit_amount: 0, credit_amount: 8000 },
|
|
{ account_number: '2611', debit_amount: 0, credit_amount: 2000 },
|
|
],
|
|
}
|
|
const result = CreateJournalEntrySchema.safeParse(entryInput)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('CreateInvoiceSchema matches makeInvoice() shape', () => {
|
|
const invoiceData = {
|
|
customer_id: validUuid,
|
|
invoice_date: '2025-01-15',
|
|
due_date: '2025-02-14',
|
|
currency: 'SEK',
|
|
items: [
|
|
{ description: 'Consulting', quantity: 10, unit: 'tim', unit_price: 1000 },
|
|
],
|
|
}
|
|
const result = CreateInvoiceSchema.safeParse(invoiceData)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('CreateSupplierInvoiceSchema matches makeSupplierInvoice() shape', () => {
|
|
const supplierInvoiceData = {
|
|
supplier_id: validUuid,
|
|
supplier_invoice_number: 'F-2025-001',
|
|
invoice_date: '2025-01-15',
|
|
due_date: '2025-02-14',
|
|
items: [
|
|
{ description: 'Materials', amount: 5000, account_number: '4010' },
|
|
],
|
|
}
|
|
const result = CreateSupplierInvoiceSchema.safeParse(supplierInvoiceData)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|