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(), }) const optionalIsoDate = isoDate.or(z.literal('')).transform(v => v || undefined).optional() export const CreateInvoiceSchema = z.object({ customer_id: uuid, invoice_date: isoDate, due_date: isoDate, delivery_date: optionalIsoDate, 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: optionalIsoDate, 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: optionalIsoDate, 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().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), notes: z.string().max(2000).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, }) // ============================================================ // Settings schemas // ============================================================ export const UpdateSettingsSchema = z.object({ entity_type: EntityTypeSchema.optional(), company_name: z.string().optional(), trade_name: z.string().nullable().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().regex(/^SE\d{12}$/, 'Momsregistreringsnummer måste vara SE följt av 12 siffror').nullable().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('')), plusgiro: z.string().regex(/^\d{1,7}-\d{1}$/, 'Ogiltigt plusgironummer').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(), phone: z.string().optional(), email: z.string().email().optional().or(z.literal('')), website: z.string().optional().or(z.literal('')), pays_salaries: z.boolean().optional(), sector_slug: z.string().nullable().optional(), // Bookkeeping lock bookkeeping_locked_through: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Ogiltigt datumformat (YYYY-MM-DD)').nullable().optional(), auto_lock_period_days: z.number().int().positive().nullable().optional(), // Voucher series default_voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), // Invoice PDF settings ore_rounding: z.boolean().optional(), invoice_show_ocr: z.boolean().optional(), invoice_show_bankgiro: z.boolean().optional(), invoice_show_plusgiro: z.boolean().optional(), invoice_late_fee_text: z.string().nullable().optional(), invoice_credit_terms_text: z.string().nullable().optional(), // AI agent flow ai_flow_enabled: z.boolean().optional(), // Agent auto-commit (low-risk ops staged by trusted agents) agent_auto_commit_enabled: z.boolean().optional(), agent_auto_commit_max_amount: z.number().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'], } ).refine( (data) => { // BFNAR 2006:1: Aktiebolag must use accrual accounting (faktureringsmetoden) if (data.entity_type === 'aktiebolag' && data.accounting_method !== undefined) { return data.accounting_method === 'accrual' } return true }, { message: 'Aktiebolag måste använda faktureringsmetoden (BFNAR 2006:1)', path: ['accounting_method'], } ) // ============================================================ // 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().nullable().optional(), default_vat_code: z.string().nullable().optional(), sru_code: z.string().nullable().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), }) // ============================================================ // Event log schemas // ============================================================ export const EventsQuerySchema = z.object({ after: z.coerce.number().int().nonnegative().optional(), types: z.string() .transform(s => s.split(',').map(t => t.trim()).filter(Boolean)) .optional(), limit: z.coerce.number().int().min(1).max(100).default(50), }) // ============================================================ // Pending operations schemas // ============================================================ export const PendingOperationsQuerySchema = z.object({ status: z.enum(['pending', 'committed', 'rejected']).default('pending'), limit: z.coerce.number().int().min(1).max(100).default(50), offset: z.coerce.number().int().nonnegative().default(0), }) // ============================================================ // Voucher gap schemas // ============================================================ export const VoucherGapQuerySchema = z.object({ fiscal_period_id: uuid, voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').optional(), }) export const SaveGapExplanationSchema = z.object({ fiscal_period_id: uuid, voucher_series: z.string().default('A'), gap_start: z.number().int().positive(), gap_end: z.number().int().positive(), explanation: z.string().min(1).max(500), }) // ============================================================ // Opening balance import schemas // ============================================================ export const OpeningBalanceExecuteSchema = z.object({ fiscal_period_id: uuid, lines: z.array(z.object({ account_number: accountNumber, debit_amount: nonNegativeAmount, credit_amount: nonNegativeAmount, })).min(2, 'At least two lines are required for double-entry'), }) // ============================================================ // Salary schemas // ============================================================ export const EmploymentTypeSchema = z.enum(['employee', 'company_owner', 'board_member']) export const SalaryTypeSchema = z.enum(['monthly', 'hourly']) export const FSkattStatusSchema = z.enum(['a_skatt', 'f_skatt', 'fa_skatt', 'not_verified']) export const VacationRuleSchema = z.enum(['procentregeln', 'sammaloneregeln']) export const SalaryRunStatusSchema = z.enum(['draft', 'review', 'approved', 'paid', 'booked', 'corrected']) export const SalaryLineItemTypeSchema = z.enum([ 'monthly_salary', 'hourly_salary', 'overtime', 'bonus', 'commission', 'gross_deduction_pension', 'gross_deduction_other', 'benefit_car', 'benefit_housing', 'benefit_meals', 'benefit_wellness', 'benefit_other', 'sick_karens', 'sick_day2_14', 'sick_day15_plus', 'vab', 'parental_leave', 'vacation', 'traktamente_taxfree', 'traktamente_taxable', 'mileage_taxfree', 'mileage_taxable', 'net_deduction_advance', 'net_deduction_union', 'net_deduction_benefit_payment', 'net_deduction_other', 'correction', 'other', ]) // Base employee object (no refinements — safe for .partial()) const EmployeeSchemaBase = z.object({ first_name: z.string().min(1).max(200), last_name: z.string().min(1).max(200), personnummer: z.string().regex(/^\d{12}$/, 'Personnummer måste vara 12 siffror (ÅÅÅÅMMDDNNNN)'), employment_type: EmploymentTypeSchema.default('employee'), employment_start: isoDate, employment_end: isoDate.optional(), employment_degree: z.number().min(1).max(100).default(100), salary_type: SalaryTypeSchema.default('monthly'), monthly_salary: z.number().nonnegative().optional(), hourly_rate: z.number().nonnegative().optional(), tax_table_number: z.number().int().min(29).max(42).optional(), tax_column: z.number().int().min(1).max(6).default(1), tax_municipality: z.string().max(100).optional(), is_sidoinkomst: z.boolean().default(false), f_skatt_status: FSkattStatusSchema.default('a_skatt'), clearing_number: z.string().max(10).optional(), bank_account_number: z.string().max(20).optional(), vacation_rule: VacationRuleSchema.default('procentregeln'), vacation_days_per_year: z.number().int().min(25).max(40).default(25), semestertillagg_rate: z.number().min(0).max(0.05).default(0.0043), email: z.string().email().optional(), phone: z.string().max(20).optional(), address_line1: z.string().max(200).optional(), postal_code: z.string().max(10).optional(), city: z.string().max(100).optional(), vaxa_stod_eligible: z.boolean().default(false), vaxa_stod_start: isoDate.optional(), vaxa_stod_end: isoDate.optional(), }) export const CreateEmployeeSchema = EmployeeSchemaBase.superRefine((data, ctx) => { // Salary amount required based on salary_type if (data.salary_type === 'monthly' && (data.monthly_salary === undefined || data.monthly_salary === null || data.monthly_salary <= 0)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Månadslön krävs och måste vara större än 0 för månadslöneform', path: ['monthly_salary'], }) } if (data.salary_type === 'hourly' && (data.hourly_rate === undefined || data.hourly_rate === null || data.hourly_rate <= 0)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Timlön krävs och måste vara större än 0 för timlöneform', path: ['hourly_rate'], }) } // Tax table required for A-skatt employees (not sidoinkomst) if (data.f_skatt_status === 'a_skatt' && !data.is_sidoinkomst && !data.tax_table_number) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Skattetabell krävs för A-skatt anställda (baseras på folkbokföringskommun)', path: ['tax_table_number'], }) } // Tax municipality recommended when tax table is set if (data.tax_table_number && !data.tax_municipality) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Folkbokföringskommun bör anges för att dokumentera skattetabellens underlag', path: ['tax_municipality'], }) } }) export const UpdateEmployeeSchema = EmployeeSchemaBase.partial().superRefine((data, ctx) => { // Only validate salary when salary_type is being changed in this update if (data.salary_type === 'monthly' && data.monthly_salary !== undefined && data.monthly_salary <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Månadslön måste vara större än 0 för månadslöneform', path: ['monthly_salary'], }) } if (data.salary_type === 'hourly' && data.hourly_rate !== undefined && data.hourly_rate <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Timlön måste vara större än 0 för timlöneform', path: ['hourly_rate'], }) } // If setting salary_type, require the corresponding salary field if (data.salary_type === 'monthly' && !('monthly_salary' in data)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Månadslön måste anges vid byte till månadslöneform', path: ['monthly_salary'], }) } if (data.salary_type === 'hourly' && !('hourly_rate' in data)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Timlön måste anges vid byte till timlöneform', path: ['hourly_rate'], }) } }) export const CreateSalaryRunSchema = z.object({ period_year: z.number().int().min(2020).max(2100), period_month: z.number().int().min(1).max(12), payment_date: isoDate, voucher_series: z.string().regex(/^[A-Z]$/, 'Verifikationsserie måste vara en bokstav A–Z').default('A'), notes: z.string().max(2000).optional(), }) export const AddEmployeeToRunSchema = z.object({ employee_id: uuid, hours_worked: z.number().nonnegative().optional(), }) export const CreateSalaryLineItemSchema = z.object({ salary_run_employee_id: uuid, item_type: SalaryLineItemTypeSchema, description: z.string().min(1).max(500), quantity: z.number().optional(), unit_price: z.number().optional(), amount: z.number(), is_taxable: z.boolean().default(true), is_avgift_basis: z.boolean().default(true), is_vacation_basis: z.boolean().default(true), is_gross_deduction: z.boolean().default(false), is_net_deduction: z.boolean().default(false), account_number: accountNumber.optional(), sort_order: z.number().int().default(0), }) export const UpdateSalaryLineItemSchema = CreateSalaryLineItemSchema.partial().omit({ salary_run_employee_id: true }) // ============================================================ // AI agent flow schemas // ============================================================ const BookingProposalLineSchema = z.object({ account_number: accountNumber, debit_amount: nonNegativeAmount, credit_amount: nonNegativeAmount, description: z.string().min(1).max(500), }) const BookingProposalCounterpartyTemplateSchema = z.object({ counterparty_name: z.string().min(1).max(200), debit_account: accountNumber, credit_account: accountNumber, vat_treatment: VatTreatmentSchema.nullable(), category: TransactionCategorySchema.nullable(), }) // Edit payload: the user's edited version of a booking proposal. Used in // the /accept endpoint when the user adjusted accounts/VAT before approving. export const EditBookingProposalSchema = z.object({ lines: z.array(BookingProposalLineSchema).min(2), vat_treatment: VatTreatmentSchema.nullable(), default_private: z.boolean(), counterparty_template_proposal: BookingProposalCounterpartyTemplateSchema.nullable(), fiscal_period_id: uuid, entry_date: isoDate, description: z.string().min(1).max(500), }) // For match proposals, editing just means picking a different transaction. export const EditMatchProposalSchema = z.object({ matched_transaction_id: uuid, }) export const AcceptProposalSchema = z.object({ version: z.number().int().nonnegative(), edits: z.union([EditBookingProposalSchema, EditMatchProposalSchema]).optional(), }) // Change the matched transaction on a pending match proposal without // accepting it. Source tells us whether the user picked one of the AI's // own alternatives, an AI-regenerated suggestion, or a manually-chosen // transaction — kept on edit_diff for learning signal. export const ChangeMatchProposalSchema = z.object({ version: z.number().int().nonnegative(), matched_transaction_id: uuid, source: z.enum(['user_alternative', 'user_manual', 'ai_regenerated']), }) export const RejectProposalSchema = z.object({ version: z.number().int().nonnegative(), reason: z.string().max(500).optional(), }) export const BatchAcceptSchema = z.object({ proposal_ids: z.array(uuid).min(1).max(50), }) export const ResolveRequestSchema = z.object({ response: z.record(z.string(), z.unknown()).optional(), }) export const StartBackfillSchema = z.object({}).strict() export const RememberLearningSchema = z.object({ proposal_id: uuid, counterparty_name: z.string().min(1).max(200), debit_account: accountNumber, credit_account: accountNumber, vat_treatment: VatTreatmentSchema.nullable(), category: TransactionCategorySchema.nullable(), }) export const ListProposalsQuerySchema = z.object({ status: z .enum(['pending', 'accepted', 'rejected', 'skipped', 'invalidated']) .optional(), step_type: z.enum(['match', 'booking']).optional(), limit: z.coerce.number().int().min(1).max(100).default(20), offset: z.coerce.number().int().min(0).default(0), })