091d043c85
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
531 lines
17 KiB
TypeScript
531 lines
17 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
// ============================================================
|
|
// Shared primitives
|
|
// ============================================================
|
|
|
|
/** UUID v4 string */
|
|
const uuid = z.string().uuid()
|
|
|
|
/** ISO date string (YYYY-MM-DD) */
|
|
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected YYYY-MM-DD date format')
|
|
|
|
/** BAS account number — always a string of 4 digits */
|
|
const accountNumber = z.string().regex(/^\d{4}$/, 'Account number must be exactly 4 digits')
|
|
|
|
/** Non-negative monetary amount (>= 0) */
|
|
const nonNegativeAmount = z.number().nonnegative()
|
|
|
|
/** Time string (HH:MM or HH:MM:SS) */
|
|
const timeString = z.string().regex(/^\d{2}:\d{2}(:\d{2})?$/, 'Expected HH:MM or HH:MM:SS time format')
|
|
|
|
// ============================================================
|
|
// Enum schemas (matching types/index.ts)
|
|
// ============================================================
|
|
|
|
export const EntityTypeSchema = z.enum(['enskild_firma', 'aktiebolag'])
|
|
|
|
export const CustomerTypeSchema = z.enum([
|
|
'individual',
|
|
'swedish_business',
|
|
'eu_business',
|
|
'non_eu_business',
|
|
])
|
|
|
|
export const SupplierTypeSchema = z.enum([
|
|
'swedish_business',
|
|
'eu_business',
|
|
'non_eu_business',
|
|
])
|
|
|
|
export const InvoiceStatusSchema = z.enum([
|
|
'draft', 'sent', 'paid', 'overdue', 'cancelled', 'credited',
|
|
])
|
|
|
|
export const InvoiceDocumentTypeSchema = z.enum([
|
|
'invoice', 'proforma', 'delivery_note',
|
|
])
|
|
|
|
export const SupplierInvoiceStatusSchema = z.enum([
|
|
'registered', 'approved', 'paid', 'partially_paid', 'overdue', 'disputed', 'credited',
|
|
])
|
|
|
|
export const VatTreatmentSchema = z.enum([
|
|
'standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt',
|
|
])
|
|
|
|
export const AccountingMethodSchema = z.enum(['accrual', 'cash'])
|
|
|
|
export const CurrencySchema = z.enum(['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK'])
|
|
|
|
export const TransactionCategorySchema = z.enum([
|
|
'income_services',
|
|
'income_products',
|
|
'income_other',
|
|
'expense_equipment',
|
|
'expense_software',
|
|
'expense_travel',
|
|
'expense_office',
|
|
'expense_marketing',
|
|
'expense_professional_services',
|
|
'expense_education',
|
|
'expense_representation',
|
|
'expense_consumables',
|
|
'expense_vehicle',
|
|
'expense_telecom',
|
|
'expense_bank_fees',
|
|
'expense_card_fees',
|
|
'expense_currency_exchange',
|
|
'expense_other',
|
|
'private',
|
|
'uncategorized',
|
|
])
|
|
|
|
export const JournalEntrySourceTypeSchema = z.enum([
|
|
'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',
|
|
'currency_revaluation',
|
|
])
|
|
|
|
export const AccountTypeSchema = z.enum([
|
|
'asset', 'equity', 'liability', 'revenue', 'expense',
|
|
])
|
|
|
|
export const NormalBalanceSchema = z.enum(['debit', 'credit'])
|
|
|
|
export const MappingRuleTypeSchema = z.enum([
|
|
'mcc_code', 'merchant_name', 'description_pattern', 'amount_threshold', 'combined',
|
|
])
|
|
|
|
export const RiskLevelSchema = z.enum(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH'])
|
|
|
|
export const DeadlineTypeSchema = z.enum([
|
|
'delivery', 'invoicing', 'report', 'tax', 'other',
|
|
])
|
|
|
|
export const DeadlinePrioritySchema = z.enum(['critical', 'important', 'normal'])
|
|
|
|
export const TaxDeadlineTypeSchema = z.enum([
|
|
'moms_monthly',
|
|
'moms_quarterly',
|
|
'moms_yearly',
|
|
'f_skatt',
|
|
'arbetsgivardeklaration',
|
|
'inkomstdeklaration_ef',
|
|
'inkomstdeklaration_ab',
|
|
'arsredovisning',
|
|
'periodisk_sammanstallning',
|
|
'bokslut',
|
|
])
|
|
|
|
export const DeadlineSourceSchema = z.enum(['system', 'user'])
|
|
|
|
export const MomsPeriodSchema = z.enum(['monthly', 'quarterly', 'yearly'])
|
|
|
|
export const DocumentUploadSourceSchema = z.enum([
|
|
'camera', 'file_upload', 'email', 'e_invoice', 'scan', 'api', 'system',
|
|
])
|
|
|
|
// ============================================================
|
|
// Invoice schemas
|
|
// ============================================================
|
|
|
|
export const CreateInvoiceItemSchema = z.object({
|
|
description: z.string().min(1, 'Item description is required'),
|
|
quantity: z.number().positive('Quantity must be positive'),
|
|
unit: z.string().min(1, 'Unit is required'),
|
|
unit_price: z.number(),
|
|
vat_rate: z.number().min(0).max(100).optional(),
|
|
})
|
|
|
|
export const CreateInvoiceSchema = z.object({
|
|
customer_id: uuid,
|
|
invoice_date: isoDate,
|
|
due_date: isoDate,
|
|
currency: CurrencySchema,
|
|
document_type: InvoiceDocumentTypeSchema.optional(),
|
|
your_reference: z.string().optional(),
|
|
our_reference: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
items: z.array(CreateInvoiceItemSchema).min(1, 'At least one item is required'),
|
|
})
|
|
|
|
export const CreateCreditNoteSchema = z.object({
|
|
credited_invoice_id: uuid,
|
|
reason: z.string().optional(),
|
|
})
|
|
|
|
export const MarkInvoicePaidSchema = z.object({
|
|
payment_date: isoDate.optional(),
|
|
exchange_rate_difference: z.number().optional(),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Customer schemas
|
|
// ============================================================
|
|
|
|
export const CreateCustomerSchema = z.object({
|
|
name: z.string().min(1, 'Customer name is required'),
|
|
customer_type: CustomerTypeSchema,
|
|
email: z.string().email('Invalid email address').optional(),
|
|
phone: z.string().optional(),
|
|
address_line1: z.string().optional(),
|
|
address_line2: z.string().optional(),
|
|
postal_code: z.string().optional(),
|
|
city: z.string().optional(),
|
|
country: z.string().optional(),
|
|
org_number: z.string().optional(),
|
|
vat_number: z.string().optional(),
|
|
default_payment_terms: z.number().int().positive().optional(),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
export const UpdateCustomerSchema = CreateCustomerSchema.partial()
|
|
|
|
// ============================================================
|
|
// Supplier schemas
|
|
// ============================================================
|
|
|
|
export const CreateSupplierSchema = z.object({
|
|
name: z.string().min(1, 'Supplier name is required'),
|
|
supplier_type: SupplierTypeSchema,
|
|
email: z.string().email('Invalid email address').optional(),
|
|
phone: z.string().optional(),
|
|
address_line1: z.string().optional(),
|
|
address_line2: z.string().optional(),
|
|
postal_code: z.string().optional(),
|
|
city: z.string().optional(),
|
|
country: z.string().optional(),
|
|
org_number: z.string().optional(),
|
|
vat_number: z.string().optional(),
|
|
bankgiro: z.string().optional(),
|
|
plusgiro: z.string().optional(),
|
|
bank_account: z.string().optional(),
|
|
iban: z.string().optional(),
|
|
bic: z.string().optional(),
|
|
default_expense_account: accountNumber.optional(),
|
|
default_payment_terms: z.number().int().positive().optional(),
|
|
default_currency: CurrencySchema.nullable().optional(),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
export const UpdateSupplierSchema = CreateSupplierSchema.partial()
|
|
|
|
// ============================================================
|
|
// Supplier invoice schemas
|
|
// ============================================================
|
|
|
|
export const CreateSupplierInvoiceItemSchema = z.object({
|
|
description: z.string().min(1, 'Item description is required'),
|
|
amount: z.number().optional(),
|
|
account_number: accountNumber,
|
|
vat_rate: z.number().min(0).max(100).optional(),
|
|
vat_code: z.string().optional(),
|
|
quantity: z.number().optional(),
|
|
unit: z.string().optional(),
|
|
unit_price: z.number().optional(),
|
|
})
|
|
|
|
export const CreateSupplierInvoiceSchema = z.object({
|
|
supplier_id: uuid,
|
|
supplier_invoice_number: z.string().min(1, 'Supplier invoice number is required'),
|
|
invoice_date: isoDate,
|
|
due_date: isoDate,
|
|
delivery_date: isoDate.optional(),
|
|
currency: CurrencySchema.optional(),
|
|
exchange_rate: z.number().positive().optional(),
|
|
vat_treatment: VatTreatmentSchema.optional(),
|
|
reverse_charge: z.boolean().optional(),
|
|
payment_reference: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
items: z.array(CreateSupplierInvoiceItemSchema).min(1, 'At least one item is required'),
|
|
})
|
|
|
|
export const MarkSupplierInvoicePaidSchema = z.object({
|
|
amount: z.number().positive().optional(),
|
|
payment_date: isoDate.optional(),
|
|
exchange_rate_difference: z.number().optional(),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
export const UpdateSupplierInvoiceSchema = z.object({
|
|
supplier_invoice_number: z.string().min(1).optional(),
|
|
invoice_date: isoDate.optional(),
|
|
due_date: isoDate.optional(),
|
|
delivery_date: isoDate.optional(),
|
|
payment_reference: z.string().optional(),
|
|
notes: z.string().optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Journal entry schemas
|
|
// ============================================================
|
|
|
|
export const CreateJournalEntryLineSchema = z.object({
|
|
account_number: accountNumber,
|
|
debit_amount: nonNegativeAmount.default(0),
|
|
credit_amount: nonNegativeAmount.default(0),
|
|
line_description: z.string().optional(),
|
|
currency: z.string().optional(),
|
|
amount_in_currency: z.number().optional(),
|
|
exchange_rate: z.number().positive().optional(),
|
|
tax_code: z.string().optional(),
|
|
cost_center: z.string().optional(),
|
|
project: z.string().optional(),
|
|
})
|
|
|
|
export const CreateJournalEntrySchema = z.object({
|
|
fiscal_period_id: uuid,
|
|
entry_date: isoDate,
|
|
description: z.string().min(1, 'Description is required'),
|
|
source_type: JournalEntrySourceTypeSchema.default('manual'),
|
|
source_id: z.string().optional(),
|
|
voucher_series: z.string().optional(),
|
|
lines: z.array(CreateJournalEntryLineSchema).min(2, 'At least two lines are required for double-entry'),
|
|
})
|
|
|
|
export const CorrectJournalEntrySchema = z.object({
|
|
lines: z.array(CreateJournalEntryLineSchema).min(2, 'At least two lines are required for double-entry'),
|
|
})
|
|
|
|
// ============================================================
|
|
// Transaction schemas
|
|
// ============================================================
|
|
|
|
export const CategorizeTransactionSchema = z.object({
|
|
is_business: z.boolean(),
|
|
category: TransactionCategorySchema.optional(),
|
|
template_id: z.string().optional(),
|
|
vat_treatment: VatTreatmentSchema.optional(),
|
|
account_override: accountNumber.optional(),
|
|
user_description: z.string().max(500).optional(),
|
|
inbox_item_id: z.string().uuid().optional(),
|
|
})
|
|
|
|
export const BookTransactionSchema = z.object({
|
|
fiscal_period_id: uuid,
|
|
entry_date: isoDate,
|
|
description: z.string().min(1, 'Description is required'),
|
|
lines: z.array(CreateJournalEntryLineSchema).min(1, 'At least one line is required'),
|
|
})
|
|
|
|
export const MatchInvoiceSchema = z.object({
|
|
invoice_id: uuid,
|
|
})
|
|
|
|
export const MatchSupplierInvoiceSchema = z.object({
|
|
supplier_invoice_id: uuid,
|
|
})
|
|
|
|
export const DescribeTransactionSchema = z.object({
|
|
description: z.string().min(3).max(500),
|
|
})
|
|
|
|
export const BatchDescribeSchema = z.object({
|
|
merchant_name: z.string().min(1),
|
|
template_id: z.string().min(1),
|
|
is_business: z.boolean(),
|
|
user_description: z.string().max(500).optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Settings schemas
|
|
// ============================================================
|
|
|
|
export const UpdateSettingsSchema = z.object({
|
|
entity_type: EntityTypeSchema.optional(),
|
|
company_name: z.string().optional(),
|
|
org_number: z.string().optional(),
|
|
address_line1: z.string().optional(),
|
|
address_line2: z.string().optional(),
|
|
postal_code: z.string().optional(),
|
|
city: z.string().optional(),
|
|
country: z.string().optional(),
|
|
f_skatt: z.boolean().optional(),
|
|
vat_registered: z.boolean().optional(),
|
|
vat_number: z.string().optional(),
|
|
moms_period: MomsPeriodSchema.nullable().optional(),
|
|
fiscal_year_start_month: z.number().int().min(1).max(12).optional(),
|
|
preliminary_tax_monthly: z.number().nullable().optional(),
|
|
bank_name: z.string().optional(),
|
|
clearing_number: z.string().optional(),
|
|
account_number: z.string().optional(),
|
|
iban: z.string().optional(),
|
|
bic: z.string().optional(),
|
|
accounting_method: AccountingMethodSchema.optional(),
|
|
invoice_prefix: z.string().nullable().optional(),
|
|
next_invoice_number: z.number().int().positive().optional(),
|
|
invoice_default_days: z.number().int().positive().optional(),
|
|
invoice_default_notes: z.string().nullable().optional(),
|
|
email: z.string().email().optional(),
|
|
pays_salaries: z.boolean().optional(),
|
|
sector_slug: z.string().nullable().optional(),
|
|
}).refine(
|
|
(data) => {
|
|
// BFL 3 kap.: Enskild firma must have fiscal year starting January
|
|
if (data.entity_type === 'enskild_firma' && data.fiscal_year_start_month !== undefined) {
|
|
return data.fiscal_year_start_month === 1
|
|
}
|
|
return true
|
|
},
|
|
{
|
|
message: 'Enskild firma must have fiscal year starting in January (BFL 3 kap.)',
|
|
path: ['fiscal_year_start_month'],
|
|
}
|
|
)
|
|
|
|
// ============================================================
|
|
// Fiscal period schemas
|
|
// ============================================================
|
|
|
|
export const CreateFiscalPeriodSchema = z.object({
|
|
name: z.string().min(1, 'Period name is required'),
|
|
period_start: isoDate,
|
|
period_end: isoDate,
|
|
}).refine(
|
|
(data) => data.period_start < data.period_end,
|
|
{
|
|
message: 'Period start must be before period end',
|
|
path: ['period_end'],
|
|
}
|
|
)
|
|
|
|
// ============================================================
|
|
// Mapping rule schemas
|
|
// ============================================================
|
|
|
|
export const CreateMappingRuleSchema = z.object({
|
|
rule_name: z.string().min(1, 'Rule name is required'),
|
|
rule_type: MappingRuleTypeSchema,
|
|
priority: z.number().int().min(0).optional(),
|
|
mcc_codes: z.array(z.string()).optional(),
|
|
merchant_pattern: z.string().optional(),
|
|
description_pattern: z.string().optional(),
|
|
amount_min: z.number().optional(),
|
|
amount_max: z.number().optional(),
|
|
debit_account: accountNumber,
|
|
credit_account: accountNumber,
|
|
vat_treatment: z.string().optional(),
|
|
risk_level: RiskLevelSchema.optional(),
|
|
default_private: z.boolean().optional(),
|
|
requires_review: z.boolean().optional(),
|
|
confidence_score: z.number().min(0).max(1).optional(),
|
|
})
|
|
|
|
export const EvaluateMappingRulesSchema = z.union([
|
|
z.object({ transaction_id: uuid }),
|
|
z.object({
|
|
description: z.string().optional(),
|
|
amount: z.number(),
|
|
}).passthrough(),
|
|
])
|
|
|
|
// ============================================================
|
|
// Deadline schemas
|
|
// ============================================================
|
|
|
|
export const CreateDeadlineSchema = z.object({
|
|
title: z.string().min(1, 'Title is required'),
|
|
due_date: isoDate,
|
|
due_time: timeString.optional(),
|
|
deadline_type: DeadlineTypeSchema,
|
|
priority: DeadlinePrioritySchema.optional(),
|
|
customer_id: uuid.optional(),
|
|
notes: z.string().optional(),
|
|
tax_deadline_type: TaxDeadlineTypeSchema.optional(),
|
|
tax_period: z.string().optional(),
|
|
source: DeadlineSourceSchema.optional(),
|
|
linked_report_type: z.string().optional(),
|
|
linked_report_period: z.record(z.string(), z.unknown()).optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Account schemas
|
|
// ============================================================
|
|
|
|
export const CreateAccountSchema = z.object({
|
|
account_number: accountNumber,
|
|
account_name: z.string().min(1, 'Account name is required'),
|
|
account_type: AccountTypeSchema,
|
|
normal_balance: NormalBalanceSchema,
|
|
plan_type: z.enum(['k1', 'full_bas']).optional(),
|
|
description: z.string().optional(),
|
|
})
|
|
|
|
export const UpdateAccountSchema = z.object({
|
|
account_name: z.string().min(1).optional(),
|
|
is_active: z.boolean().optional(),
|
|
description: z.string().nullable().optional(),
|
|
default_vat_code: z.string().nullable().optional(),
|
|
sru_code: z.string().nullable().optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Bank reconciliation schemas
|
|
// ============================================================
|
|
|
|
export const BankLinkSchema = z.object({
|
|
transaction_id: uuid,
|
|
journal_entry_id: uuid,
|
|
})
|
|
|
|
export const BankUnlinkSchema = z.object({
|
|
transaction_id: uuid,
|
|
})
|
|
|
|
export const RunReconciliationSchema = z.object({
|
|
date_from: isoDate.optional(),
|
|
date_to: isoDate.optional(),
|
|
dry_run: z.boolean().optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Report query schemas
|
|
// ============================================================
|
|
|
|
export const VatDeclarationQuerySchema = z.object({
|
|
periodType: z.enum(['monthly', 'quarterly', 'yearly']),
|
|
year: z.coerce.number().int().min(2000).max(2100),
|
|
period: z.coerce.number().int().min(1).max(12),
|
|
})
|
|
|
|
export const ReportPeriodQuerySchema = z.object({
|
|
fiscal_period_id: uuid.optional(),
|
|
year: z.coerce.number().int().min(2000).max(2100).optional(),
|
|
month: z.coerce.number().int().min(1).max(12).optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// VAT validation schemas
|
|
// ============================================================
|
|
|
|
export const ValidateVatNumberSchema = z.object({
|
|
vat_number: z.string().min(4, 'VAT number must be at least 4 characters'),
|
|
customer_id: uuid.optional(),
|
|
})
|
|
|
|
// ============================================================
|
|
// Pagination schemas
|
|
// ============================================================
|
|
|
|
export const PaginationQuerySchema = z.object({
|
|
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
offset: z.coerce.number().int().nonnegative().default(0),
|
|
})
|