* feat: separate AR/AP/accounting into distinct nav groups (#92) Split the flat "Finans" sidebar group into three visually distinct sections — Försäljning (AR), Inköp (AP), and Redovisning — so users coming from Fortnox immediately find customer invoicing and supplier invoices as top-level concepts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: journal entry detail view, correction chain, and account name display - Add journal entry detail page at /bookkeeping/[id] with full entry view - Add correction chain API and component showing storno relationships - Add JournalEntryStatusBadge component for entry status display - Show debit/credit account names in template picker and review dialogs - Expand client-side BAS account name mapping with additional accounts - Show account codes on transaction inbox suggestion buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — N+1 query, duplicate name, nav dedup - Batch reverse-lookup into single query per BFS iteration (was N+1) - Differentiate account 2393 from 2893 in display names - Extract shared loop for desktop/mobile nav group rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: counterparty templates, Skatteverket extension, VAT form completeness, and UI cleanup - Add counterparty-based categorization templates (learned from user approvals and auto-ingestion) with fuzzy matching in the mapping engine - Add Skatteverket extension for direct VAT declaration submission via API - Complete VAT declaration form with all 30 SKV 4700 boxes (ruta 08, 35-42, 50, 60-62) - Fix ruta 49 formula to include import VAT (ruta 60+61+62) - Simplify dashboard UI: remove redundant icons from stat cards, customer cards, invoice list, supplier invoices; use Badge variants consistently - Add SkatteverketPanel component to reports page - Add categorization_templates and skatteverket_tokens migrations - Update tests and helpers for new types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback — VAT detection, migration timestamps, dedup - Fix detectVatTreatment to derive actual rate (12%/6%) from VAT line description instead of hardcoding standard_25 - Rename skatteverket_tokens migration to 20260324120001 to avoid duplicate timestamp with categorization_templates (fixes Supabase deployment failure) - Make refreshAccessToken accept previousRefreshCount param to enforce refresh limit contract at the type level - Fix rate limiter TOCTOU by claiming slot before await - Extract formatRedovisare/formatRedovisningsperiod to shared lib/skatteverket/format.ts — eliminates duplication between mappers.ts and SkatteverketPanel.tsx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: multi-line counterparty templates, batch matching, settings restructure Counterparty template engine: - Multi-line booking patterns (line_pattern JSONB) for complex entries with split VAT, tax accounts, and ratio-based allocation - Batch matching (findCounterpartyTemplatesBatch) — 1 DB query for all transactions instead of up to 3 per transaction - SIE voucher population (populateTemplatesFromSieVouchers) — extracts patterns from historical vouchers on import with dominance filtering - Source priority system (user_approved > sie_import > auto_learned) - Centralized counterparty: prefix helpers to prevent string fragility - Fix: re-approval path now updates line_pattern Transaction categorization: - /describe route returns counterparty_match in parallel with templates/AI - /categorize route accepts counterparty_template_id for direct booking - /suggest-categories uses batch matching, injects counterparty suggestions - transaction-entries supports all_lines_complete for multi-line patterns UI: - TemplatePicker shows "Tidigare motparter" section (no AI extension needed) - DescribeTransactionDialog shows counterparty match card with detail - QuickReviewDialog supports counterparty line patterns - JournalEntryPreview renders multi-line patterns with VAT/ratio math - Inline LinePatternEntry types replaced with shared import from @/types Settings restructure: - 8 tabs → 5: merged Säkerhet + Utseende + Kalender into Konto - Renamed "Motparter" → "Mallar" - CounterpartyTemplatesPanel: click-to-expand detail view with account lines, VAT, confidence, aliases, and delete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — account_override guard, DELETE body parsing, stale test - Block account_override when counterparty_template_id is set (prevents corrupting stored template via override → upsert correction path) - Wrap DELETE request.json() in try-catch for malformed body (400 not 500) - Clean up stale 3-query mock enqueues in test for batch-based find Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
539 lines
18 KiB
TypeScript
539 lines
18 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(),
|
|
lines: z.array(z.object({
|
|
account_number: accountNumber,
|
|
debit_amount: nonNegativeAmount.default(0),
|
|
credit_amount: nonNegativeAmount.default(0),
|
|
line_description: z.string().optional(),
|
|
})).min(2).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(),
|
|
counterparty_template_id: z.string().uuid().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().max(100, 'Banknamn får vara max 100 tecken').optional(),
|
|
clearing_number: z.string().regex(/^\d{4,5}$/, 'Clearingnummer måste vara 4-5 siffror').optional().or(z.literal('')),
|
|
account_number: z.string().regex(/^\d{6,12}$/, 'Kontonummer måste vara 6-12 siffror').optional().or(z.literal('')),
|
|
bankgiro: z.string().regex(/^(\d{3,4}-\d{4}|\d{7,8})$/, 'Ogiltigt bankgironummer (7-8 siffror)').nullable().optional().or(z.literal('')),
|
|
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.nullish(),
|
|
deadline_type: DeadlineTypeSchema,
|
|
priority: DeadlinePrioritySchema.nullish(),
|
|
customer_id: uuid.nullish(),
|
|
notes: z.string().nullish(),
|
|
tax_deadline_type: TaxDeadlineTypeSchema.nullish(),
|
|
tax_period: z.string().nullish(),
|
|
source: DeadlineSourceSchema.optional(),
|
|
linked_report_type: z.string().nullish(),
|
|
linked_report_period: z.record(z.string(), z.unknown()).nullish(),
|
|
})
|
|
|
|
// ============================================================
|
|
// 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),
|
|
})
|