     1|import { apiGet, apiPost } from './client'
     2|
     3|export type BudgetAccount = { budget_account_id: string; name: string; account_type: string; currency: string; is_active: boolean; linked_account_id?: string | null; archived_at?: string | null; notes?: string | null }
     4|export type BudgetCategory = { category_id: string; parent_category_id?: string | null; name: string; category_type: string; is_active: boolean; sort_order?: number; color?: string | null; icon?: string | null; transaction_count?: number; expense_sum_chf?: string | number; active_plan_count?: number; merchant_count?: number; rule_count?: number; children?: BudgetCategory[] }
     5|export type BudgetTag = { tag_id: string; name: string; color?: string | null; is_active: boolean }
     6|export type BudgetTransaction = { budget_transaction_id: string; account_id?: string; account_name?: string; category_id?: string | null; category_name?: string | null; category_icon?: string | null; category_color?: string | null; transaction_date: string; transaction_date_group?: string; transaction_type: string; description: string; payee?: string | null; merchant_display_name?: string | null; amount_original: string; currency_original: string; amount_chf?: string | null; fx_status?: string; source_type?: string; source_candidate_id?: string | null; status: string; tag_names?: string[]; line_item_count?: number; article_rows_detail_only?: boolean }
     7|export type BudgetTransfer = { transfer_id: string; transaction_date: string; from_account_id: string; to_account_id: string; from_account_name: string; to_account_name: string; description: string; amount_original: string; currency_original: string; transfer_type: string; status: string; notes?: string | null }
     8|export type BudgetMonthlyAnalysis = { month: string; month_label: string; actual_chf: string; budget_chf: string; difference_chf: string; status: string; bar_color: 'green' | 'yellow' | 'red' | 'slate' | string }
     9|export type BudgetMonthDetail = { category_id: string; category: string; category_type: string; month: string; budget_month: string; actual_chf: string; difference_chf: string; difference_percent: string; status: string; bar_color: string; breakdown_mode: string; breakdown: Array<{ label: string; amount_chf: string; share_percent: string }>; transactions: BudgetTransaction[]; expense_filter_url: string }
    10|export type BudgetPlanningRow = { category_id: string; category: string; category_type: string; budget_month_chf?: string | null; budget_year_chf?: string | null; budget_cadence?: string | null; has_budget: boolean; actual_current_month_chf: string; actual_months_2026: Record<string, string>; actual_year_to_date_chf: string; actual_avg_month_chf: string; linear_forecast_chf: string; budget_adjusted_forecast_chf: string; forecast_year_chf: string; previous_year_months: Record<string, string>; previous_year_total_chf: string; previous_year_monthly_average_chf: string; has_previous_year: boolean; actual_vs_budget_chf: string; forecast_vs_budget_chf: string; current_month_vs_budget_chf: string; current_month_vs_previous_year_chf: string; ytd_vs_previous_year_chf: string; forecast_vs_previous_year_chf: string; status: string; status_color: string; plausibility_checks?: Array<{ severity: string; code: string; message: string }>; data_explorer_url: string; effective_expenses_url: string; monthly_detail_url: string; monthly_chart: Array<{ month: string; actual_2026_chf: string; budget_chf: string; actual_2025_chf: string; deviation_chf: string }> }
    11|export type BudgetPlanningMatrix = { purpose: 'budget_planning_forecast_v1'; year: string; previous_year: string; current_month: string; forecast_method: string; forecast_method_help?: Record<string, string>; rows: BudgetPlanningRow[]; categories_without_budget: string[]; categories_without_previous_year: string[]; categories_with_actuals_without_budget: string[]; categories_with_budget_without_actuals: string[]; categories_with_previous_year_without_budget?: string[] }
    12|export type BudgetPlanningAudit = { category_id: string; category?: string; entries: Array<{ timestamp: string; action_label: string; source: string; note?: string; created_by: string; changes: Array<{ field_label: string; old_value: string; new_value: string }>; technical?: { audit_id: string } }> }
    13|export type BudgetRecurringPayment = { recurring_id: string; name: string; merchant_name?: string | null; category_id: string; category_name?: string | null; account_id?: string | null; merchant_id?: string | null; expected_amount_text: string; expected_amount_chf?: string; currency: string; frequency: string; expected_day_of_month?: number | null; expected_month?: number | null; tolerance_amount_text?: string | null; tolerance_percent?: string | null; amount_tolerance_pct: string; date_tolerance_days: number; recurring_type: string; status: string; source: string; confidence?: string; last_seen_date?: string | null; next_expected_date?: string | null; notes?: string | null; budget_month_chf?: string; budget_year_chf?: string }
    14|export type BudgetRecurringDashboard = { purpose: 'fixed_costs_subscriptions_v1'; candidates: BudgetRecurringPayment[]; active: BudgetRecurringPayment[]; warnings: Array<Record<string, string>>; category_breakdown: Array<Record<string, string | number>>; kpis?: { monthly_fixed_costs_chf?: string; annual_fixed_costs_chf?: string; fixed_cost_quote_percent?: string; active_recurring_count?: number; open_candidate_count?: number; missing_payment_count?: number; next_due?: BudgetRecurringPayment | null; largest_fixed_costs?: BudgetRecurringPayment[] }; analytics: Record<string, any> }
    15|export type BudgetImportStatusAudit = { sources: Record<string, Record<string, number>>; quality: Record<string, number>; akb_raiffeisen: Record<string, any> }
    16|export type ReviewBacklogMerchantGroup = { merchant_key: string; merchant_name: string; count: number; sources: string[]; candidate_ids: string[] }
    17|export type BudgetReviewBacklog = { purpose: 'review_backlog_cleanup_v1'; audit: BudgetImportStatusAudit; header: Record<string, number>; groups: Record<string, any>; confirm_groups: Record<string, any> }
    18|export type BudgetImportSession = { import_session_id: string; source?: string; file_name?: string; profile?: string; rows_total: number; new_candidates: number; already_known: number; duplicate_count: number; ignored_count: number; covered_by_source_count: number; error_count: number; status: string; review_url?: string; created_at: string }
    19|export type BudgetMonthlyImportDashboard = { purpose: 'budget_monthly_import_dashboard_v1'; latest_import_by_source: Record<string, BudgetImportSession>; open_candidates_by_source: Record<string, number>; counts: Record<string, number>; rule_suggestions_open: number; review_url: string }
    20|export type BudgetMonthlyImportResult = { purpose: string; status: string; files_total: number; session_ids: string[]; files: Array<Record<string, unknown>>; totals: Record<string, number>; review_url: string }
    21|export type BudgetUploadImportResult = { purpose: string; preview_id?: string; import_session_id?: string; status: string; file?: { detected: boolean; name: string; source_hash?: string }; profile?: string | null; source_type?: string | null; period?: { start?: string | null; end?: string | null }; totals: Record<string, number>; review_url: string; warnings?: string[]; errors?: string[] }
    22|export type BudgetRuleSuggestion = { suggestion_id: string; merchant_pattern: string; match_type: string; source_scope: string; category_id?: string | null; category_name?: string | null; recurring_type?: string | null; confidence: string; affected_open_candidate_count: number; status: string; created_at: string }
    23|export type GroceryMapping = { mapping_id: string; target_retailer: string; target_product_name: string; target_product_url: string; status: string; match_type?: string; confidence?: string; cache_status?: string; last_price_checked_at?: string; target_price_text?: string; target_unit_price_text?: string; target_price_date?: string; target_price_source?: string }
    24|export type GroceryManualMappingPreview = { preview_id: string; product_item_id: string; target_retailer: string; target_product_name: string; target_product_url: string; target_package_size?: string; target_unit?: string; candidate_price_text?: string; candidate_unit_price_text?: string; target_price_date?: string; target_price_source?: string; quality_flags: string[]; can_count_as_secure_saving: boolean; requires_confirm: boolean }
    25|export type GroceryProductItem = { product_item_id: string; receipt_id: string; purchase_date: string; store_name: string; raw_product_name: string; normalized_product_name: string; quantity_text?: string; unit?: string; unit_price_text?: string; total_price_text: string; action_label?: string; include_in_analysis: number; health_analysis_status: string; known_mappings?: GroceryMapping[] }
    26|export type GroceryReceipt = { receipt_id: string; purchase_date: string; store_name: string; article_count: number; total_text: string; items: GroceryProductItem[] }
    27|export type GroceryDashboard = { purpose: 'migros_product_savings_grocery_optimizer_v1'; receipts: GroceryReceipt[]; supported_retailers: string[]; health_analysis_status: string; disclaimer: string }
    28|export type GroceryRun = { run_id: string; receipt_id: string; original_total_text: string; optimized_total_text: string; quality_status: string; summary: Record<string, any>; optimized_lists: Array<Record<string, any>>; product_comparisons: Array<Record<string, any>>; health_analysis_status: string }
    29|export type GroceryOnlinePriceMatch = { match_id?: string; mapping_id?: string; product_item_id: string; retailer: string; product_name: string; product_url: string; price_text?: string; unit_price_text?: string; fetched_at?: string; quality_flags?: string[]; confidence?: string; status: string; source?: string; cache_status?: string }
    30|export type GroceryOnlinePriceSearch = { receipt_id: string; provider_calls: number; cache_hits: number; matches: GroceryOnlinePriceMatch[] }
    31|export type BudgetPreview = { preview_id: string; summary: string; fx_status?: string; warnings: string[]; payload: Record<string, unknown>; review?: Record<string, string>; requires_explicit_confirm?: boolean }
    32|export type BudgetConfirm = { status: string; entity_id: string; audit_id: string; message: string }
    33|export type BudgetSeedCandidate = {
    34|  seed_candidate_id: string
    35|  source_file_label?: string
    36|  source_sheet: string
    37|  source_row_or_range?: string | null
    38|  candidate_type: 'category' | 'budget_plan' | 'recurring_candidate' | 'unclean_range'
    39|  source_label?: string | null
    40|  proposed_category_id?: string | null
    41|  proposed_parent_label?: string | null
    42|  proposed_name?: string | null
    43|  proposed_period_type?: string | null
    44|  proposed_amount_text?: string | null
    45|  currency?: string | null
    46|  confidence: string
    47|  requires_review: boolean
    48|  status: string
    49|  notes?: string | null
    50|}
    51|export type BudgetSeedSummary = { category_candidate_count: number; budget_plan_candidate_count: number; recurring_candidate_count: number; unclean_range_count: number; current_budget_sheet?: string }
    52|export type BudgetStatusRow = { purpose?: string; category_id: string; category: string; category_type: string; budget_month?: string | null; budget_year?: string | null; has_budget?: boolean; actual_current_month?: string; actual_year_to_date: string; monthly_actuals?: Record<string, string>; monthly_analysis?: BudgetMonthlyAnalysis[]; actual_avg_month: string; forecast_year: string; linear_forecast?: string; forecast_avg_month: string; deviation_year: string; deviation_month: string; actual_2025_year?: string; previous_year_total?: string; previous_year_months?: Record<string, string>; deviation_previous_year?: string; has_previous_year?: boolean; data_explorer_url?: string; effective_expenses_url?: string; monthly_chart?: Array<Record<string, string>>; status: string; status_hint?: string; months_with_bookings: number; plan_names?: string[]; is_rollup?: boolean; rollup_child_count?: number }
    53|export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null; linked_candidate_id?: string | null }
    54|export type BudgetReviewSummary = { open_total: number; auto_categorized: number; review_needed: number; galaxus_digitec: number; migros_over_50: number; covered_by_migros: number; unclear: number; possible_duplicates: number }
    55|export type BudgetRule = { rule_id: string; merchant_contains: string; source_type?: string | null; category_id?: string | null; category_name?: string | null; target_status: string; is_active: boolean; priority?: number; confidence?: string | null; notes?: string | null }
    56|export type BudgetMerchant = { merchant_id: string; display_name: string; normalized_name: string; default_category_id?: string | null; default_category_name?: string | null; is_active: boolean; notes?: string | null; alias_count?: number; open_candidate_count?: number }
    57|export type BudgetCharts = { year: string; empty_state: boolean; cashflow_by_month: Array<{ year: string; month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string }>; budget_vs_actual_by_category: Array<{ year: string; category_id: string; category: string; category_type: string; budget_year?: string | null; actual_year_to_date: string; forecast_year: string; status: string }>; forecast_overruns: Array<Record<string, string>> }
    58|export type CategoryAnalysis = { year: string; month?: string | null; mode: 'income' | 'expense'; total_chf: string; categories: Array<{ category_id: string; category: string; amount_chf: string; share_percent: string; transaction_count: number; color?: string | null; icon?: string | null; transactions_url: string }> }
    59|export type MonthlyComparison = { year: string; months: Array<{ month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string; savings_rate_percent?: string; cumulative_income_chf?: string; cumulative_expense_chf?: string; transaction_count: number; top_categories: Array<{ category: string; amount_chf: string }>; top_merchants: Array<{ merchant: string; amount_chf: string }>; largest_transactions?: Array<Record<string, string>>; transactions_url: string }> }
    60|export type BudgetDataExplorer = { purpose: 'budget_data_explorer'; filters: Record<string, string>; kpis: { income_sum_chf: string; expense_sum_chf: string; net_cashflow_chf: string; transaction_count: number; average_transaction_chf: string; largest_transaction_chf: string; largest_transaction_id?: string | null; top_category: string; top_merchant: string }; transactions: BudgetTransaction[]; top_merchants: Array<{ merchant: string; transaction_count: number; sum_chf: string; average_chf: string; last_transaction_date?: string | null; top_category: string; change_vs_previous_month_chf: string }>; fixed_variable: Record<string, string>; export: { enabled: boolean; target: string; message: string } }
    61|export type IncomePlan = { plan_item_id: string; name: string; person_source: string; employer_source?: string; income_type: string; target_month?: string; cadence: string; sort_order?: number; monthly_value_chf: string; annual_value_chf: string; monthly_average_chf?: string; actual_income_year_to_date_chf?: string; forecast_income_year_chf?: string; deviation_year_chf?: string; status_label?: string; category_id: string; category_name: string; is_active: boolean; is_template?: boolean; notes?: string | null }
    62|export type IncomeSeedCandidate = { seed_candidate_id: string; candidate_type: string; proposed_name?: string | null; proposed_parent_label?: string | null; confidence: string; requires_review: boolean; status: string; source_sheet?: string; source_row_or_range?: string | null }
    63|export type BudgetOverview = { current_month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string; review_count: number; uncategorized_count: number; pending_seed_candidate_count?: number; pending_transaction_candidate_count?: number; planned_income_chf?: string; planned_expense_chf?: string; planned_net_chf?: string; actual_income_chf?: string; actual_expense_chf: string; actual_net_chf?: string; actuals_label?: string; open_categories_without_actuals?: number; planned_budget_chf: string; remaining_budget_chf: string; budget_status: string; budget_categories: BudgetStatusRow[]; uncategorized_transactions: BudgetTransaction[]; top_categories: Array<{ category_id?: string; category_name: string; amount_chf: string; transaction_count?: number }>; recent_transactions: BudgetTransaction[] }
    64|export type BudgetDashboardCockpit = { purpose: 'monthly_cockpit'; month: string; year: string; chart_library: string; kpis: { income_current_month_chf: string; expense_current_month_chf: string; net_cashflow_current_month_chf: string; budget_consumption_percent: string; savings_rate_percent: string; open_review_transactions: number; fixed_costs_month_chf?: string; fixed_cost_quote_percent?: string; open_fixed_cost_candidate_count?: number; missing_fixed_cost_payment_count?: number }; monthly_cashflow: { income_chf: string; expense_chf: string; net_cashflow_chf: string }; top_expense_categories: Array<{ category_id: string; category: string; amount_chf: string; share_percent: string; transaction_count: number; color?: string | null; icon?: string | null }>; critical_categories: BudgetStatusRow[]; recent_confirmed_expenses: BudgetTransaction[]; open_todos: { review_candidates: number; categories_without_budget: number; unconfirmed_transactions: number } }
    65|export type BudgetVsActualAnalysis = { purpose: 'plan_vs_reality_analysis'; year: string; chart_library: string; empty_state: boolean; budget_vs_actual_by_category: BudgetStatusRow[]; top_overruns: Array<Record<string, string>>; top_under_budget: Array<Record<string, string>>; cashflow_by_month: Array<{ month: string; income_chf: string; expense_chf: string; net_cashflow_chf: string }>; category_donut: Array<{ category_id: string; category: string; amount_chf: string; share_percent: string; color?: string | null }>; trend_by_month: Array<{ month: string; cumulative_income_chf: string; cumulative_expense_chf: string; cumulative_net_chf: string }> }
    66|
    67|export const getBudgetDashboardCockpit = (month = '2026-05') => apiGet<BudgetDashboardCockpit>(`/api/budget/dashboard/cockpit?month=${month}`)
    68|export const getBudgetPlanningMatrix = (year = '2026', currentMonth = '') => apiGet<BudgetPlanningMatrix>(`/api/budget/planning/matrix?year=${year}${currentMonth ? `&current_month=${encodeURIComponent(currentMonth)}` : ''}`)
    69|export const getBudgetPlanningAudit = (categoryId: string) => apiGet<BudgetPlanningAudit>(`/api/budget/planning/categories/${encodeURIComponent(categoryId)}/audit`)
    70|export const previewBudgetPlanningCategory = (categoryId: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/planning/categories/${encodeURIComponent(categoryId)}/preview`, body)
    71|export const confirmBudgetPlanningCategory = (categoryId: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/planning/categories/${encodeURIComponent(categoryId)}/confirm`, body)
    72|export const previewBudgetPlanningBatch = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/planning/batch/preview', body)
    73|export const confirmBudgetPlanningBatch = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/planning/batch/confirm', body)
    74|export const previewBudgetPlanningExcelTemplate = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/planning/excel-template/preview', body)
    75|export const getBudgetRecurringDashboard = () => apiGet<BudgetRecurringDashboard>('/api/budget/recurring')
    76|export const getBudgetImportStatusAudit = () => apiGet<BudgetImportStatusAudit>('/api/budget/import-status-audit')
    77|export const getBudgetReviewBacklog = () => apiGet<BudgetReviewBacklog>('/api/budget/review-backlog')
    78|export const markBudgetBacklog2025Reference = () => apiPost<Record<string, unknown>>('/api/budget/review-backlog/mark-2025-reference', {})
    79|export const getBudgetMonthlyImportDashboard = () => apiGet<BudgetMonthlyImportDashboard>('/api/budget/monthly-import/dashboard')
    80|export const getGroceryOptimizerDashboard = () => apiGet<GroceryDashboard>('/api/budget/grocery-optimizer/dashboard')
    81|export const getGroceryProviderStrategy = () => apiGet<Record<string, unknown>>('/api/budget/grocery-optimizer/provider-strategy')
    82|export const getGroceryRappnLink = (productName: string) => apiGet<Record<string, unknown>>(`/api/budget/grocery-optimizer/rappn-link?product_name=${encodeURIComponent(productName)}`)
    83|export const searchOpenPricesSpike = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/open-prices-spike', body)
    84|export const createGroceryDemoReceipt = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/demo-receipt', body)
    85|export const addGroceryProductMatch = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/matches', body)
    86|export const searchGroceryOnlinePrices = (body: unknown) => apiPost<GroceryOnlinePriceSearch>('/api/budget/grocery-optimizer/search-prices', body)
    87|export const acceptGroceryProductMatch = (body: { match_id: string; save_as_preferred?: boolean; user_note?: string }) => apiPost<Record<string, unknown>>(`/api/budget/grocery-optimizer/matches/${encodeURIComponent(body.match_id)}/accept`, body)
    88|export const rejectGroceryProductMatch = (body: { match_id: string; user_note?: string }) => apiPost<Record<string, unknown>>(`/api/budget/grocery-optimizer/matches/${encodeURIComponent(body.match_id)}/reject`, body)
    89|export const previewManualGroceryMapping = (body: unknown) => apiPost<GroceryManualMappingPreview>('/api/budget/grocery-optimizer/manual-mapping/preview', body)
    90|export const confirmManualGroceryMapping = (body: GroceryManualMappingPreview) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/manual-mapping/confirm', body)
    91|export const saveManualGroceryMapping = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/manual-mapping', body)
    92|export const refreshGroceryMappingPrice = (body: { mapping_id: string }) => apiPost<Record<string, unknown>>(`/api/budget/grocery-optimizer/mappings/${encodeURIComponent(body.mapping_id)}/refresh-price`, {})
    93|export const runGroceryOptimization = (body: unknown) => apiPost<GroceryRun>('/api/budget/grocery-optimizer/optimize', body)
    94|export const generateGroceryOptimizationReport = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/grocery-optimizer/report', body)
    95|export const getBudgetImportHistory = () => apiGet<BudgetImportSession[]>('/api/budget/monthly-import/history')
    96|export const getBudgetImportSessions = () => apiGet<BudgetImportSession[]>('/api/budget/import/sessions')
    97|export const previewBudgetUploadImport = (body: unknown) => apiPost<BudgetUploadImportResult>('/api/budget/import/upload/preview', body)
    98|export const confirmBudgetUploadImportCandidates = (body: unknown) => apiPost<BudgetUploadImportResult>('/api/budget/import/upload/confirm-candidates', body)
    99|export const scanBudgetMonthlyImportDrive = (body: unknown = {}) => apiPost<Record<string, unknown>>('/api/budget/monthly-import/drive-scan', body)
   100|export const dryRunBudgetMonthlyImport = (body: unknown) => apiPost<BudgetMonthlyImportResult>('/api/budget/monthly-import/dry-run', body)
   101|export const confirmBudgetMonthlyImportCandidates = (body: unknown) => apiPost<BudgetMonthlyImportResult>('/api/budget/monthly-import/confirm-candidates', body)
   102|export const getBudgetRuleSuggestions = () => apiGet<BudgetRuleSuggestion[]>('/api/budget/rules/suggestions')
   103|export const applyBudgetRuleSuggestion = (id: string, applyToOpen = true) => apiPost<Record<string, unknown>>(`/api/budget/rules/suggestions/${encodeURIComponent(id)}/apply`, { apply_to_open: applyToOpen })
   104|export const rejectBudgetRuleSuggestion = (id: string, later = false) => apiPost<Record<string, unknown>>(`/api/budget/rules/suggestions/${encodeURIComponent(id)}/reject`, { later })
   105|export const detectBudgetRecurringCandidates = () => apiPost<Record<string, unknown>>('/api/budget/recurring/detect', {})
   106|export const previewBudgetRecurringCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/recurring/candidates/${encodeURIComponent(id)}/preview`, body)
   107|export const confirmBudgetRecurringCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/recurring/candidates/${encodeURIComponent(id)}/confirm`, body)
   108|export const previewManualRecurringPayment = (body: unknown) => apiPost<BudgetPreview>('/api/budget/recurring/manual/preview', body)
   109|export const confirmManualRecurringPayment = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/recurring/manual/confirm', body)
   110|export const archiveRecurringPayment = (id: string) => apiPost<BudgetConfirm>(`/api/budget/recurring/${encodeURIComponent(id)}/archive`, {})
   111|export const getBudgetOverview = (month = '2026-01') => apiGet<BudgetOverview>(`/api/budget/overview?month=${month}`)
   112|export const getBudgetStatusByCategory = (year = '2026') => apiGet<BudgetStatusRow[]>(`/api/budget/status/categories?year=${year}`)
   113|export const getCategoryMonthDetail = (categoryId: string, month: string) => apiGet<BudgetMonthDetail>(`/api/budget/status/categories/${encodeURIComponent(categoryId)}/months/${encodeURIComponent(month)}`)
   114|export const getBudgetCharts = (year = '2026') => apiGet<BudgetCharts>(`/api/budget/charts?year=${year}`)
   115|export const getBudgetVsActualAnalysis = (year = '2026') => apiGet<BudgetVsActualAnalysis>(`/api/budget/analysis/budget-vs-actual?year=${year}`)
   116|export const getCategoryAnalysis = (year = '2026', txType: 'income' | 'expense' = 'expense', month = '') => {
   117|  const params = new URLSearchParams({ year, tx_type: txType })
   118|  if (month) params.set('month', month)
   119|  return apiGet<CategoryAnalysis>(`/api/budget/analysis/categories?${params.toString()}`)
   120|}
   121|export const getMonthlyComparison = (year = '2026') => apiGet<MonthlyComparison>(`/api/budget/analysis/monthly-comparison?year=${year}`)
   122|export const getBudgetDataExplorer = (filters: Record<string, string> = {}) => {
   123|  const params = new URLSearchParams()
   124|  Object.entries(filters).forEach(([key, value]) => { if (value) params.set(key, value) })
   125|  return apiGet<BudgetDataExplorer>(`/api/budget/analysis/data-explorer?${params.toString()}`)
   126|}
   127|export const getIncomeStatus = (year = '2026') => apiGet<Record<string, unknown>>(`/api/budget/income/status?year=${year}`)
   128|export const getIncomePlans = (year = '2026') => apiGet<IncomePlan[]>(`/api/budget/income/plans?year=${year}`)
   129|export const getIncomeSeedCandidates = (year = '2026') => apiGet<IncomeSeedCandidate[]>(`/api/budget/income/seed-candidates?year=${year}`)
   130|export const previewIncomePlan = (body: unknown) => apiPost<BudgetPreview>('/api/budget/income/plans/preview', body)
   131|export const confirmIncomePlan = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/income/plans/confirm', body)
   132|export const previewIncomeTemplates = (year = '2026') => apiPost<Record<string, unknown>>('/api/budget/income/templates/preview', { year })
   133|export const confirmIncomeTemplates = (year = '2026') => apiPost<Record<string, unknown>>('/api/budget/income/templates/confirm', { year })
   134|export const previewIncomePlanUpdate = (id: string, body: unknown) => apiPost<BudgetPreview & { requires_explicit_confirm?: boolean }>(`/api/budget/income/plans/${id}/update/preview`, body)
   135|export const confirmIncomePlanUpdate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/income/plans/${id}/update/confirm`, body)
   136|export const reorderIncomePlans = (plan_item_ids: string[], year = '2026') => apiPost<BudgetConfirm & { updated_count: number }>('/api/budget/income/plans/reorder', { plan_item_ids, year })
   137|export const archiveIncomePlan = (id: string) => apiPost<BudgetConfirm>(`/api/budget/income/plans/${id}/archive`, {})
   138|export const getBudgetTransactions = (query = '') => apiGet<BudgetTransaction[]>(`/api/budget/transactions${query}`)
   139|export const getBudgetTransfers = (query = '') => apiGet<BudgetTransfer[]>(`/api/budget/transfers${query}`)
   140|export const getBudgetAccounts = () => apiGet<BudgetAccount[]>('/api/budget/accounts')
   141|export const getBudgetCategories = () => apiGet<BudgetCategory[]>('/api/budget/categories')
   142|export const getBudgetTags = () => apiGet<BudgetTag[]>('/api/budget/tags')
   143|export const previewBudgetTransaction = (body: unknown) => apiPost<BudgetPreview>('/api/budget/transactions/preview', body)
   144|export const confirmBudgetTransaction = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/transactions/confirm', body)
   145|export const updateBudgetTransaction = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transactions/${id}/update`, body)
   146|export const previewBudgetTransfer = (body: unknown) => apiPost<BudgetPreview>('/api/budget/transfers/preview', body)
   147|export const confirmBudgetTransfer = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/transfers/confirm', body)
   148|export const previewBudgetAccount = (body: unknown) => apiPost<BudgetPreview>('/api/budget/accounts/preview', body)
   149|export const confirmBudgetAccount = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/accounts/confirm', body)
   150|export const archiveBudgetAccount = (id: string) => apiPost<BudgetConfirm>(`/api/budget/accounts/${id}/archive`, {})
   151|export const previewBudgetCategory = (body: unknown) => apiPost<BudgetPreview>('/api/budget/categories/preview', body)
   152|export const confirmBudgetCategory = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/categories/confirm', body)
   153|export const previewBudgetCategoryCleanup = () => apiPost<Record<string, unknown> & { preview_id: string; summary: string; target_expense_categories: string[]; auto_mappings: Array<Record<string, unknown>>; review_required: Array<Record<string, unknown>>; empty_categories: Array<Record<string, unknown>>; payload: Record<string, unknown>; warnings: string[] }>('/api/budget/categories/cleanup/preview', {})
   154|export const confirmBudgetCategoryCleanup = (body: unknown) => apiPost<Record<string, unknown> & { audit_id: string }>('/api/budget/categories/cleanup/confirm', body)
   155|export const reorderBudgetCategories = (category_ids: string[], category_type: 'expense' | 'income' = 'expense') => apiPost<BudgetConfirm & { updated_count: number }>('/api/budget/categories/reorder', { category_ids, category_type })
   156|export const updateBudgetCategory = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/update`, body)
   157|export const archiveBudgetCategory = (id: string) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/archive`, {})
   158|export const previewBudgetTag = (body: unknown) => apiPost<BudgetPreview>('/api/budget/tags/preview', body)
   159|export const confirmBudgetTag = (body: unknown) => apiPost<BudgetConfirm>('/api/budget/tags/confirm', body)
   160|export const archiveBudgetTag = (id: string) => apiPost<BudgetConfirm>(`/api/budget/tags/${id}/archive`, {})
   161|export const getBudgetSeedCandidates = () => apiGet<BudgetSeedCandidate[]>('/api/budget/seed-candidates')
   162|export const generateBudgetSeedCandidates = (body: unknown) => apiPost<BudgetSeedSummary>('/api/budget/seed-candidates/generate', body)
   163|export const previewBudgetSeedCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/seed-candidates/${id}/preview`, body)
   164|export const confirmBudgetSeedCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/confirm`, body)
   165|export const ignoreBudgetSeedCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/ignore`, { note })
   166|export const reopenBudgetSeedCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/reopen`, { note })
   167|export const setBudgetSeedCandidateStatus = (id: string, status: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/status`, { status, note })
   168|export const convertBudgetUncleanRange = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/seed-candidates/${id}/convert-unclean`, body)
   169|export const previewClearBudgetCategoriesTakeover = () => apiPost<{ preview_id: string; summary: string; count: number; categories: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/seed-candidates/categories/preview-clear-takeover', {})
   170|export const confirmClearBudgetCategoriesTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/categories/confirm-clear-takeover', {})
   171|export const previewBudgetPlanTakeover = () => apiPost<{ preview_id: string; summary: string; count: number; budget_plans: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/seed-candidates/budget-plans/preview-takeover', {})
   172|export const confirmBudgetPlanTakeover = () => apiPost<BudgetConfirm & { confirmed_count: number; skipped_count: number }>('/api/budget/seed-candidates/budget-plans/confirm-takeover', {})
   173|export const getBudgetTransactionCandidates = (status = '', tab = '', filters: Record<string, string> = {}) => {
   174|  const params = new URLSearchParams()
   175|  if (status) params.set('status', status)
   176|  if (tab) params.set('tab', tab)
   177|  Object.entries(filters).forEach(([k, v]) => { if (v) params.set(k, v) })
   178|  const qs = params.toString()
   179|  return apiGet<BudgetTransactionCandidate[]>(`/api/budget/transaction-candidates${qs ? `?${qs}` : ''}`)
   180|}
   181|

Use offset=181 to continue reading (showing 1-180 of 219 lines)


   181|export const getBudgetTransactionCandidateSummary = (budget_year = '2026') => apiGet<BudgetReviewSummary>(`/api/budget/transaction-candidates/summary?budget_year=${budget_year}`)
   182|export const generateBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, number>>('/api/budget/transaction-candidates/generate', body)
   183|export const previewBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetPreview>(`/api/budget/transaction-candidates/${id}/preview`, body)
   184|export const confirmBudgetTransactionCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/confirm`, body)
   185|export const confirmBudgetTransactionCandidateDuplicateOverride = (id: string, body: unknown) => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/confirm-duplicate-override`, body)
   186|export const ignoreBudgetTransactionCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/ignore`, { note })
   187|export const reopenBudgetTransactionCandidate = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/reopen`, { note })
   188|export const updateBudgetTransactionCandidateCategory = (id: string, category_id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/category`, { category_id, note })
   189|export const bulkUpdateBudgetTransactionCandidateCategory = (candidate_ids: string[], category_id: string, note = '') => apiPost<{ updated_count: number; skipped_count: number }>('/api/budget/transaction-candidates/bulk-category', { candidate_ids, category_id, note })
   190|export const splitBudgetTransactionCandidate = (id: string, splits: unknown[]) => apiPost<BudgetConfirm & { split_count: number }>(`/api/budget/transaction-candidates/${id}/split`, { splits })
   191|export const markBudgetTransactionCandidateTransfer = (id: string, note = '') => apiPost<BudgetConfirm>(`/api/budget/transaction-candidates/${id}/transfer`, { note })
   192|export const markBudgetTransactionCandidateType = (id: string, candidate_type: 'transfer' | 'investment_transfer' | 'credit_card_payment', note = '') => apiPost<BudgetConfirm & { classification: string }>(`/api/budget/transaction-candidates/${id}/type`, { candidate_type, note })
   193|export const getBudgetImportProfiles = () => apiGet<Array<Record<string, unknown>>>('/api/budget/import-profiles')
   194|export const dryRunBudgetImportProfile = (profile: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/import-profiles/${profile}/dry-run`, body)
   195|export const createBudgetImportCandidates = (profile: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/import-profiles/${profile}/candidates`, body)
   196|export const previewSafeBudgetTransactionCandidates = () => apiPost<{ preview_id: string; summary: string; count: number; groups: Array<Record<string, unknown>>; warnings: string[] }>('/api/budget/transaction-candidates/batch-confirm-safe/preview', {})
   197|export const batchConfirmSafeBudgetTransactionCandidates = (account_id: string, candidate_ids: string[] = []) => apiPost<{ confirmed_count: number; skipped_count: number }>('/api/budget/transaction-candidates/batch-confirm-safe', { account_id, candidate_ids })
   198|export const deleteEmptyBudgetCategory = (id: string) => apiPost<BudgetConfirm>(`/api/budget/categories/${id}/delete-empty`, {})
   199|export const previewBudgetCategoryMerges = () => apiPost<{ preview_id: string; summary: string; suggestions: Array<{ target_category_id: string; target_name: string; source_category_ids: string[]; source_names: string[] }>; warnings: string[] }>('/api/budget/categories/merge/preview', {})
   200|export const confirmBudgetCategoryMerge = (body: unknown) => apiPost<BudgetConfirm & { moved_transactions: number; moved_budget_plans: number; moved_children: number }>('/api/budget/categories/merge/confirm', body)
   201|export const previewBatchBudgetTransactionCandidates = (candidate_ids: string[], account_id: string, budget_year = '2026') => apiPost<{ preview_id: string; count: number; categories: Record<string, number>; sources?: Record<string, number>; possible_duplicates?: number; blockers?: Record<string, number>; conflicts: Array<Record<string, unknown>>; can_confirm?: boolean; requires_explicit_confirm: boolean }>('/api/budget/transaction-candidates/batch/preview', { candidate_ids, account_id, budget_year })
   202|export const batchBudgetTransactionCandidates = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/transaction-candidates/batch', body)
   203|export const confirmBudgetTransactionCandidateSplit = (id: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/transaction-candidates/${id}/split/confirm`, body)
   204|export const createReviewCategoryForCandidate = (id: string, body: unknown) => apiPost<BudgetConfirm & { category_id: string }>(`/api/budget/transaction-candidates/${id}/category/create`, body)
   205|export const getBudgetRules = () => apiGet<BudgetRule[]>('/api/budget/rules')
   206|export const createBudgetRule = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/rules', body)
   207|export const updateBudgetRule = (id: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/update`, body)
   208|export const prioritizeBudgetRule = (id: string, priority: number) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/priority`, { priority })
   209|export const deactivateBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/deactivate`, {})
   210|export const testBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/test`, {})
   211|export const applyBudgetRule = (id: string) => apiPost<Record<string, unknown>>(`/api/budget/rules/${id}/apply`, {})
   212|export const getBudgetMerchants = () => apiGet<BudgetMerchant[]>('/api/budget/merchants')
   213|export const createBudgetMerchant = (body: unknown) => apiPost<Record<string, unknown>>('/api/budget/merchants', body)
   214|export const createBudgetMerchantAlias = (merchantId: string, body: unknown) => apiPost<Record<string, unknown>>(`/api/budget/merchants/${merchantId}/aliases`, body)
   215|export const updateBudgetMerchantCategory = (merchantId: string, categoryId: string) => apiPost<Record<string, unknown>>(`/api/budget/merchants/${merchantId}/category`, { category_id: categoryId })
   216|export const applyBudgetMerchant = (merchantId: string) => apiPost<Record<string, unknown>>(`/api/budget/merchants/${merchantId}/apply`, {})
   217|export const mergeBudgetMerchants = (source_merchant_id: string, target_merchant_id: string) => apiPost<Record<string, unknown>>('/api/budget/merchants/merge', { source_merchant_id, target_merchant_id })
   218|export const detectBudgetDuplicateCandidates = () => apiPost<Record<string, unknown>>('/api/budget/transaction-candidates/detect-duplicates', {})
   219|export const reclassifyBankIncomeCandidates = (budget_year = '2026') => apiPost<Record<string, number>>('/api/budget/transaction-candidates/bank-income/reclassify', { budget_year })
   220|


Explanation:
**Considering frontend unit tests**

I'm thinking about adding a frontend unit test. It seems like I'll need to set up some testing infrastructure, potentially using PrimeVue. I should check which components I need to test to ensure everything works smoothly. It's essential to have a solid testing foundation, so I want to make sure I'm covering all the necessary elements. Let’s explore this and see what steps I need to take for implementation!


     1|import { mount, flushPromises } from '@vue/test-utils'
     2|import { beforeEach, describe, expect, it, vi } from 'vitest'
     3|import BudgetImportReviewPage from './BudgetImportReviewPage.vue'
     4|import { getBudgetAccounts, getBudgetCategories, getBudgetTransactionCandidateSummary, getBudgetTransactionCandidates, markBudgetTransactionCandidateType, batchBudgetTransactionCandidates, previewBatchBudgetTransactionCandidates, confirmBudgetTransactionCandidateDuplicateOverride } from '@/api/budget'
     5|
     6|vi.mock('primevue/usetoast', () => ({ useToast: () => ({ add: vi.fn() }) }))
     7|vi.mock('primevue/useconfirm', () => ({ useConfirm: () => ({ require: ({ accept }: { accept: () => void }) => accept() }) }))
     8|vi.mock('@/api/budget', () => ({
     9|  getBudgetAccounts: vi.fn(),
    10|  getBudgetCategories: vi.fn(),
    11|  getBudgetTransactionCandidateSummary: vi.fn(),
    12|  getBudgetTransactionCandidates: vi.fn(),
    13|  updateBudgetTransactionCandidateCategory: vi.fn(),
    14|  previewBatchBudgetTransactionCandidates: vi.fn(),
    15|  batchBudgetTransactionCandidates: vi.fn(),
    16|  confirmBudgetTransactionCandidate: vi.fn(),
    17|  confirmBudgetTransactionCandidateDuplicateOverride: vi.fn(),
    18|  ignoreBudgetTransactionCandidate: vi.fn(),
    19|  confirmBudgetTransactionCandidateSplit: vi.fn(),
    20|  createReviewCategoryForCandidate: vi.fn(),
    21|  markBudgetTransactionCandidateType: vi.fn()
    22|}))
    23|
    24|beforeEach(() => {
    25|  vi.clearAllMocks()
    26|  vi.mocked(getBudgetAccounts).mockResolvedValue([{ budget_account_id: 'acc1', name: 'Haushalt', account_type: 'checking', currency: 'CHF', is_active: true }] as any)
    27|  vi.mocked(getBudgetCategories).mockResolvedValue([{ category_id: 'cat1', name: 'Essen & Haushalt', category_type: 'expense', is_active: true, children: [] }, { category_id: 'cat_media', name: 'Elektronische Medien', category_type: 'expense', is_active: true, children: [] }] as any)
    28|  vi.mocked(getBudgetTransactionCandidateSummary).mockResolvedValue({ open_total: 2, auto_categorized: 0, review_needed: 1, galaxus_digitec: 0, migros_over_50: 0, covered_by_migros: 0, unclear: 0, possible_duplicates: 1 } as any)
    29|  vi.mocked(getBudgetTransactionCandidates).mockResolvedValue([
    30|    { transaction_candidate_id: 'cand1', source_file_label: 'visa.csv', source_type: 'credit_card_csv', transaction_date: '2026-03-02', description: 'APPLE.COM/BILL', merchant: 'Apple', amount_original: '10.00', currency_original: 'CHF', proposed_category_id: 'cat_media', proposed_category_name: 'Elektronische Medien', confidence: '0.90', requires_review: false, status: 'pending', classification: 'subscription_auto', review_reason: 'user_category_override' },
    31|    { transaction_candidate_id: 'cand2', source_file_label: 'visa.csv', source_type: 'credit_card_csv', transaction_date: '2026-03-03', description: 'GOOGLE STORAGE', merchant: 'Google', amount_original: '2.00', currency_original: 'CHF', confidence: '0.80', requires_review: true, status: 'duplicate_candidate', classification: 'subscription_auto', review_reason: 'duplicate_detection_v1' }
    32|  ] as any)
    33|  vi.mocked(markBudgetTransactionCandidateType).mockResolvedValue({ status: 'transfer_candidate', classification: 'credit_card_payment', entity_id: 'cand1', audit_id: 'audit1', message: 'OK' } as any)
    34|  vi.mocked(batchBudgetTransactionCandidates).mockResolvedValue({ updated_count: 2, skipped_count: 0, confirmed_count: 2, ignored_count: 2, reopened_count: 2, audit_id: 'audit_batch', errors: [] } as any)
    35|  vi.mocked(previewBatchBudgetTransactionCandidates).mockResolvedValue({ preview_id: 'batchprev1', count: 1, categories: { 'Elektronische Medien': 1 }, sources: { credit_card_csv: 1 }, possible_duplicates: 1, blockers: { duplicate_candidates: 1, covered_by_source: 0, transfer_candidates: 0, without_category: 0, missing_required_fields: 0, not_confirmable: 0 }, conflicts: [{ candidate_id: 'cand2', reason: 'duplicate_candidate_blocked' }], can_confirm: false, requires_explicit_confirm: true } as any)
    36|  vi.mocked(confirmBudgetTransactionCandidateDuplicateOverride).mockResolvedValue({ status: 'confirmed', entity_id: 'tx_dup', audit_id: 'audit_dup', message: 'Duplikat-Override bestätigt' } as any)
    37|})
    38|
    39|describe('Budget Import Production v2 review UI', () => {
    40|  it('shows source-aware review tabs and candidate type actions', async () => {
    41|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    42|    await flushPromises()
    43|
    44|    expect(wrapper.text()).toContain('VISA')
    45|    expect(wrapper.text()).toContain('Migros')
    46|    expect(wrapper.text()).toContain('Raiffeisen')
    47|    expect(wrapper.text()).toContain('AKB')
    48|    expect(wrapper.text()).toContain('Kreditkartenausgleich')
    49|    expect(wrapper.text()).toContain('True Wealth / Investment Transfers')
    50|    expect(wrapper.text()).toContain('subscription_auto')
    51|  })
    52|
    53|  it('marks a candidate as credit card payment through audited type action', async () => {
    54|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    55|    await flushPromises()
    56|
    57|    await (wrapper.vm as any).markType({ transaction_candidate_id: 'cand1' }, 'credit_card_payment')
    58|    await flushPromises()
    59|
    60|    expect(markBudgetTransactionCandidateType).toHaveBeenCalledWith('cand1', 'credit_card_payment', 'User marked credit_card_payment')
    61|  })
    62|
    63|  it('shows bulk toolbar state, applies category to selected candidates, and keeps partial feedback visible', async () => {
    64|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    65|    await flushPromises()
    66|    expect(wrapper.get('[data-test="bulk-selection-count"]').text()).toContain('Ausgewählt: 0')
    67|    expect('disabled' in wrapper.get('[data-test="bulk-apply-category"]').attributes() || wrapper.get('[data-test="bulk-apply-category"]').attributes('aria-disabled') === 'true').toBe(true)
    68|
    69|    ;(wrapper.vm as any).selected = ['cand1', 'cand2']
    70|    ;(wrapper.vm as any).bulkCategory = 'cat_media'
    71|    await flushPromises()
    72|    await (wrapper.vm as any).bulkSetCategory()
    73|    await flushPromises()
    74|
    75|    expect(batchBudgetTransactionCandidates).toHaveBeenCalledWith({ candidate_ids: ['cand1', 'cand2'], action: 'category', category_id: 'cat_media', budget_year: '2026', note: 'Bulk category assignment' })
    76|    expect(wrapper.text()).toContain('Kategorie auf Auswahl angewendet')
    77|  })
    78|
    79|  it('opens bulk confirm preview dialog and disables final confirm when blockers exist', async () => {
    80|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    81|    await flushPromises()
    82|    ;(wrapper.vm as any).selected = ['cand1', 'cand2']
    83|    ;(wrapper.vm as any).confirmAccount = 'acc1'
    84|    await (wrapper.vm as any).previewBatchConfirm()
    85|    await flushPromises()
    86|
    87|    expect(previewBatchBudgetTransactionCandidates).toHaveBeenCalledWith(['cand1', 'cand2'], 'acc1', '2026')
    88|    expect(wrapper.text()).toContain('Bulk Confirm Preview')
    89|    expect(wrapper.text()).toContain('duplicate_candidate_blocked')
    90|    expect(wrapper.text()).toContain('Elektronische Medien')
    91|    expect('disabled' in wrapper.get('[data-test="bulk-confirm-final"]').attributes() || wrapper.get('[data-test="bulk-confirm-final"]').attributes('aria-disabled') === 'true').toBe(true)
    92|  })
    93|
    94|  it('keeps duplicate candidates out of normal confirm actions and shows duplicate review actions only', async () => {
    95|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    96|    await flushPromises()
    97|
    98|    expect(wrapper.text()).toContain('Duplikate prüfen')
    99|    expect(wrapper.text()).toContain('Mögliches Duplikat')
   100|    expect(wrapper.text()).not.toContain('Trotzdem bestätigen')
   101|    expect(wrapper.text()).toContain('Als Duplikat ignorieren')
   102|    expect(wrapper.text()).toContain('Zurück in Review')
   103|    expect(wrapper.text()).toContain('Override mit Begründung')
   104|  })
   105|
   106|  it('requires a reason and calls the duplicate override endpoint explicitly', async () => {
   107|    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
   108|    await flushPromises()
   109|    ;(wrapper.vm as any).confirmAccount = 'acc1'
   110|    ;(wrapper.vm as any).openDuplicateOverride({ transaction_candidate_id: 'cand2', status: 'duplicate_candidate', transaction_date: '2026-03-03', proposed_category_id: 'cat1', description: 'GOOGLE STORAGE', merchant: 'Google' })
   111|    await flushPromises()
   112|    ;(wrapper.vm as any).duplicateOverrideReason = 'Bewusst separater Einkauf trotz Treffer'
   113|    await (wrapper.vm as any).confirmDuplicateOverride()
   114|    await flushPromises()
   115|
   116|    expect(confirmBudgetTransactionCandidateDuplicateOverride).toHaveBeenCalledWith('cand2', { account_id: 'acc1', category_id: 'cat1', transaction_type: 'expense', budget_year: '2026', reason: 'Bewusst separater Einkauf trotz Treffer' })
   117|  })
   118|})
   119|


Explanation:
**Evaluating imports and tests**

I’m wondering if I need to add a mock confirm for the test. It looks like the confirmBudgetTransactionCandidate isn't imported correctly, which might be why the tests aren't working. I think I should import it properly to ensure the mock properties function as intended. I also need to update the type fields and think about adding a UI test for coverage. It's getting a bit complex with all the updates!

patch

export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null; linked_candidate_id?: string | null }

export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null; linked_candidate_id?: string | null; transfer_pair_status?: string | null; transfer_target_account_name?: string | null; transfer_from_account_name?: string | null; counterparty_candidate_id?: string | null; budget_impact?: string | null }


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts
@@ -50,7 +50,7 @@
 }
 export type BudgetSeedSummary = { category_candidate_count: number; budget_plan_candidate_count: number; recurring_candidate_count: number; unclean_range_count: number; current_budget_sheet?: string }
 export type BudgetStatusRow = { purpose?: string; category_id: string; category: string; category_type: string; budget_month?: string | null; budget_year?: string | null; has_budget?: boolean; actual_current_month?: string; actual_year_to_date: string; monthly_actuals?: Record<string, string>; monthly_analysis?: BudgetMonthlyAnalysis[]; actual_avg_month: string; forecast_year: string; linear_forecast?: string; forecast_avg_month: string; deviation_year: string; deviation_month: string; actual_2025_year?: string; previous_year_total?: string; previous_year_months?: Record<string, string>; deviation_previous_year?: string; has_previous_year?: boolean; data_explorer_url?: string; effective_expenses_url?: string; monthly_chart?: Array<Record<string, string>>; status: string; status_hint?: string; months_with_bookings: number; plan_names?: string[]; is_rollup?: boolean; rollup_child_count?: number }
-export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null; linked_candidate_id?: string | null }
+export type BudgetTransactionCandidate = { transaction_candidate_id: string; source_file_label: string; source_row_or_range?: string | null; source_type: string; transaction_date?: string | null; description: string; merchant?: string | null; amount_original?: string | null; currency_original: string; proposed_category_id?: string | null; proposed_category_name?: string | null; duplicate_of_transaction_id?: string | null; confidence: string; requires_review: boolean; status: string; notes?: string | null; classification?: string | null; review_reason?: string | null; rule_id?: string | null; rule_name?: string | null; account_source?: string | null; covered_by_source?: string | null; linked_candidate_id?: string | null; transfer_pair_status?: string | null; transfer_target_account_name?: string | null; transfer_from_account_name?: string | null; counterparty_candidate_id?: string | null; budget_impact?: string | null }
 export type BudgetReviewSummary = { open_total: number; auto_categorized: number; review_needed: number; galaxus_digitec: number; migros_over_50: number; covered_by_migros: number; unclear: number; possible_duplicates: number }
 export type BudgetRule = { rule_id: string; merchant_contains: string; source_type?: string | null; category_id?: string | null; category_name?: string | null; target_status: string; is_active: boolean; priority?: number; confidence?: string | null; notes?: string | null }
 export type BudgetMerchant = { merchant_id: string; display_name: string; normalized_name: string; default_category_id?: string | null; default_category_name?: string | null; is_active: boolean; notes?: string | null; alias_count?: number; open_candidate_count?: number }

npx not usable: This is not the tsc command you are looking for

/home/agent/.hermes/repos/FinanceManager/frontend/src/api/budget.ts was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.


function suggestedTargetAccount(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); if (item.transfer_target_account_name) return item.transfer_target_account_name; if (c === 'investment_transfer') return 'True Wealth Cash'; if (c === 'credit_card_payment') return 'VISA/Kreditkarte'; if (c.includes('transfer') || item.status === 'transfer_candidate') return 'Haushaltskonto/Gegenkonto'; return '—' }
function counterBookingHint(item: BudgetTransactionCandidate) { if (item.transfer_pair_status === 'matched' || item.linked_candidate_id) return 'Gegenbuchung gefunden'; if (isTransferCandidate(item)) return 'Gegenbuchung nicht gefunden'; return 'nein/noch nicht verknüpft' }
function isTransferCandidate(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); return item.status === 'transfer_candidate' || c.includes('transfer') }

function suggestedTargetAccount(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); if (c === 'investment_transfer') return 'True Wealth Cash'; if (c === 'credit_card_payment') return 'VISA/Kreditkarte'; if (c.includes('transfer') || item.status === 'transfer_candidate') return 'Haushaltskonto/Gegenkonto'; return '—' }
function counterBookingHint(item: BudgetTransactionCandidate) { return item.linked_candidate_id ? 'ja' : 'nein/noch nicht verknüpft' }


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue
@@ -151,8 +151,9 @@
 function statusLabel(value: string) { return statusLabelMap[value] || value || '—' }
 function statusSeverity(value: string) { return value.includes('review') || value.includes('pending') || value.includes('duplicate') ? 'warn' : value.includes('ignored') ? 'secondary' : value.includes('transfer') ? 'info' : 'success' }
 function transferHint(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); if (c === 'investment_transfer') return 'zählt nicht in Budget-Ausgaben/Einnahmen · Ziel: True Wealth Cash'; if (c === 'credit_card_payment') return 'Kreditkartenausgleich verhindert Doppelzählung'; if (c.includes('transfer') || item.status === 'transfer_candidate') return 'interner Transfer · kein Einkommen/keine Ausgabe'; return 'produktive Buchung erst nach Confirm' }
-function suggestedTargetAccount(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); if (c === 'investment_transfer') return 'True Wealth Cash'; if (c === 'credit_card_payment') return 'VISA/Kreditkarte'; if (c.includes('transfer') || item.status === 'transfer_candidate') return 'Haushaltskonto/Gegenkonto'; return '—' }
-function counterBookingHint(item: BudgetTransactionCandidate) { return item.linked_candidate_id ? 'ja' : 'nein/noch nicht verknüpft' }
+function suggestedTargetAccount(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); if (item.transfer_target_account_name) return item.transfer_target_account_name; if (c === 'investment_transfer') return 'True Wealth Cash'; if (c === 'credit_card_payment') return 'VISA/Kreditkarte'; if (c.includes('transfer') || item.status === 'transfer_candidate') return 'Haushaltskonto/Gegenkonto'; return '—' }
+function counterBookingHint(item: BudgetTransactionCandidate) { if (item.transfer_pair_status === 'matched' || item.linked_candidate_id) return 'Gegenbuchung gefunden'; if (isTransferCandidate(item)) return 'Gegenbuchung nicht gefunden'; return 'nein/noch nicht verknüpft' }
+function isTransferCandidate(item: BudgetTransactionCandidate) { const c = String(item.classification || ''); return item.status === 'transfer_candidate' || c.includes('transfer') }
 function typeLabel(item: BudgetTransactionCandidate) { const c = item.classification || ''; if (c === 'income_candidate') return 'income'; if (c === 'investment_transfer') return 'investment_transfer'; if (c === 'credit_card_payment') return 'credit_card_payment'; if (c.includes('transfer')) return 'transfer'; if (item.status?.startsWith('covered')) return 'covered_by_source'; return 'expense' }
 async function load() { await apply({ ...filters, status: status.value }, true); await reviewStore.load(filters, status.value, activeTab.value); selected.value = selected.value.filter(id => candidates.value.some(c => c.transaction_candidate_id === id)); batchPreview.value = null }
 async function refreshAll() { categories.value = await getBudgetCategories(); accounts.value = await getBudgetAccounts(); await load() }

No linter for .vue files

/home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.


async function confirmItem(item: BudgetTransactionCandidate) { if (!confirmAccount.value) { error.value = 'Confirm-Konto fehlt.'; notifyError(error.value); return } askRisky('Diesen Kandidaten produktiv bestätigen? Audit wird geschrieben.', async () => { try { const isIncome = ['income_candidate','possible_income'].includes(String(item.classification || '')); const result = await reviewStore.confirmCandidate(item.transaction_candidate_id, { account_id: confirmAccount.value, category_id: item.proposed_category_id, transaction_type: isIncome ? 'income' : 'expense', budget_year: filters.budget_year }); message.value = `Kandidat bestätigt. Audit: ${(result as any)?.audit_id ?? 'geschrieben'}`; error.value=''; notifySuccess(message.value) } catch (e) { error.value = e instanceof Error ? e.message : 'Confirm fehlgeschlagen'; notifyError(error.value) } }) }
async function confirmTransferItem(item: BudgetTransactionCandidate) { if (!confirmAccount.value) { error.value = 'Von-Konto fehlt.'; notifyError(error.value); return } if (!transferTargetAccount.value) { error.value = 'Transfer-Zielkonto fehlt.'; notifyError(error.value); return } askRisky('Internen Transfer bestätigen? Keine Budget-Ausgabe/Einnahme wird erzeugt, Audit wird geschrieben.', async () => { try { const result = await reviewStore.confirmCandidate(item.transaction_candidate_id, { from_account_id: confirmAccount.value, to_account_id: transferTargetAccount.value, transaction_type: 'transfer', budget_year: filters.budget_year }); message.value = `Transfer bestätigt. Audit: ${(result as any)?.audit_id ?? 'geschrieben'}`; error.value=''; notifySuccess(message.value); await load() } catch (e) { error.value = e instanceof Error ? e.message : 'Transfer-Confirm fehlgeschlagen'; notifyError(error.value) } }) }
async function setCategory

async function confirmItem(item: BudgetTransactionCandidate) { if (!confirmAccount.value) { error.value = 'Confirm-Konto fehlt.'; notifyError(error.value); return } askRisky('Diesen Kandidaten produktiv bestätigen? Audit wird geschrieben.', async () => { try { const isIncome = ['income_candidate','possible_income'].includes(String(item.classification || '')); const result = await reviewStore.confirmCandidate(item.transaction_candidate_id, { account_id: confirmAccount.value, category_id: item.proposed_category_id, transaction_type: isIncome ? 'income' : 'expense', budget_year: filters.budget_year }); message.value = `Kandidat bestätigt. Audit: ${(result as any)?.audit_id ?? 'geschrieben'}`; error.value=''; notifySuccess(message.value) } catch (e) { error.value = e instanceof Error ? e.message : 'Confirm fehlgeschlagen'; notifyError(error.value) } }) }
async function setCategory


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportReviewPage.vue
@@ -163,6 +163,7 @@
 function isDuplicateStatus(value: string | undefined) { return ['duplicate_candidate','duplicate','possible_duplicate','duplicate_blocked'].includes(String(value || '')) }
 function canConfirm(item: BudgetTransactionCandidate) { return !['covered_by_migros','covered_by_source','auto_ignored_duplicate','superseded','transfer_candidate','duplicate_candidate','duplicate','possible_duplicate','duplicate_blocked','confirmed','ignored','reference_2025','archived_reference'].includes(item.status) && String(item.transaction_date || '').startsWith(filters.budget_year) && (!!item.proposed_category_id || ['income_candidate','possible_income'].includes(String(item.classification || ''))) }
 async function confirmItem(item: BudgetTransactionCandidate) { if (!confirmAccount.value) { error.value = 'Confirm-Konto fehlt.'; notifyError(error.value); return } askRisky('Diesen Kandidaten produktiv bestätigen? Audit wird geschrieben.', async () => { try { const isIncome = ['income_candidate','possible_income'].includes(String(item.classification || '')); const result = await reviewStore.confirmCandidate(item.transaction_candidate_id, { account_id: confirmAccount.value, category_id: item.proposed_category_id, transaction_type: isIncome ? 'income' : 'expense', budget_year: filters.budget_year }); message.value = `Kandidat bestätigt. Audit: ${(result as any)?.audit_id ?? 'geschrieben'}`; error.value=''; notifySuccess(message.value) } catch (e) { error.value = e instanceof Error ? e.message : 'Confirm fehlgeschlagen'; notifyError(error.value) } }) }
+async function confirmTransferItem(item: BudgetTransactionCandidate) { if (!confirmAccount.value) { error.value = 'Von-Konto fehlt.'; notifyError(error.value); return } if (!transferTargetAccount.value) { error.value = 'Transfer-Zielkonto fehlt.'; notifyError(error.value); return } askRisky('Internen Transfer bestätigen? Keine Budget-Ausgabe/Einnahme wird erzeugt, Audit wird geschrieben.', async () => { try { const result = await reviewStore.confirmCandidate(item.transaction_candidate_id, { from_account_id: confirmAccount.value, to_account_id: transferTargetAccount.value, transaction_type: 'transfer', budget_year: filters.budget_year }); message.value = `Transfer bestätigt. Audit: ${(result as any)?.audit_id ?? 'geschrieben'}`; error.value=''; notifySuccess(message.value); await load() } catch (e) { error.value = e instanceof Error ? e.message : 'Transfer-Confirm fehlgeschlagen'; notifyError(error.value) } }) }
 async function setCategory(item: BudgetTransactionCandidate, categoryId: string) { if (!categoryId) return; await updateBudgetTransactionCandidateCategory(item.transaction_candidate_id, categoryId, 'Inline category override'); message.value = 'Kategorie aktualisiert.'; await load() }
 async function bulkSetCategory() { const result = await batchBudgetTransactionCandidates({ candidate_ids: selected.value, action: 'category', category_id: bulkCategory.value, budget_year: filters.budget_year, note: 'Bulk category assignment' }); batchErrors.value = ((result as any).errors || []) as any; const updated = (result as any).updated_count ?? 0; const skipped = (result as any).skipped_count ?? 0; message.value=`Kategorie auf Auswahl angewendet: ${updated} aktualisiert${skipped ? `, ${skipped} blockiert` : ''}.`; notifySuccess(message.value); await load() }
 async function previewBatchConfirm() { if (!confirmAccount.value) { error.value='Confirm-Konto fehlt.'; return } batchErrors.value=[]; batchPreview.value = await previewBatchBudgetTransactionCandidates(selected.value, confirmAccount.value, filters.budget_year); batchDialogVisible.value = true; error.value='' }

No linter for .vue files


Explanation:
**Planning implementation steps**

I think I need to consider whether to automatically set `transferTarget` if AKB is found, but making it optional could be better. After accounts load, if it's empty, I could set it to the matching one. I'll also need to check if I can manually choose Raiffeisen for `confirmAccount`. Plus, I should investigate the API type reclassify endpoint—even if it might not require a frontend. Testing will be essential, including mocking the transfer for the UI.

import { getBudgetAccounts, getBudgetCategories, getBudgetTransactionCandidateSummary, getBudgetTransactionCandidates, markBudgetTransactionCandidateType, batchBudgetTransactionCandidates, previewBatchBudgetTransactionCandidates, confirmBudgetTransactionCandidate, confirmBudgetTransactionCandidateDuplicateOverride } from '@/api/budget'

import { getBudgetAccounts, getBudgetCategories, getBudgetTransactionCandidateSummary, getBudgetTransactionCandidates, markBudgetTransactionCandidateType, batchBudgetTransactionCandidates, previewBatchBudgetTransactionCandidates, confirmBudgetTransactionCandidateDuplicateOverride } from '@/api/budget'


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
@@ -1,7 +1,7 @@
 import { mount, flushPromises } from '@vue/test-utils'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import BudgetImportReviewPage from './BudgetImportReviewPage.vue'
-import { getBudgetAccounts, getBudgetCategories, getBudgetTransactionCandidateSummary, getBudgetTransactionCandidates, markBudgetTransactionCandidateType, batchBudgetTransactionCandidates, previewBatchBudgetTransactionCandidates, confirmBudgetTransactionCandidateDuplicateOverride } from '@/api/budget'
+import { getBudgetAccounts, getBudgetCategories, getBudgetTransactionCandidateSummary, getBudgetTransactionCandidates, markBudgetTransactionCandidateType, batchBudgetTransactionCandidates, previewBatchBudgetTransactionCandidates, confirmBudgetTransactionCandidate, confirmBudgetTransactionCandidateDuplicateOverride } from '@/api/budget'

 vi.mock('primevue/usetoast', () => ({ useToast: () => ({ add: vi.fn() }) }))
 vi.mock('primevue/useconfirm', () => ({ useConfirm: () => ({ require: ({ accept }: { accept: () => void }) => accept() }) }))


vi.mocked(getBudgetAccounts).mockResolvedValue([{ budget_account_id: 'acc1', name: 'Haushalt', account_type: 'checking', currency: 'CHF', is_active: true }, { budget_account_id: 'acc_akb', name: 'AKB Haushaltskonto', account_type: 'cash', currency: 'CHF', is_active: true }] as any)

vi.mocked(getBudgetAccounts).mockResolvedValue([{ budget_account_id: 'acc1', name: 'Haushalt', account_type: 'checking', currency: 'CHF', is_active: true }] as any)


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
@@ -23,7 +23,7 @@

 beforeEach(() => {
   vi.clearAllMocks()
-  vi.mocked(getBudgetAccounts).mockResolvedValue([{ budget_account_id: 'acc1', name: 'Haushalt', account_type: 'checking', currency: 'CHF', is_active: true }] as any)
+  vi.mocked(getBudgetAccounts).mockResolvedValue([{ budget_account_id: 'acc1', name: 'Haushalt', account_type: 'checking', currency: 'CHF', is_active: true }, { budget_account_id: 'acc_akb', name: 'AKB Haushaltskonto', account_type: 'cash', currency: 'CHF', is_active: true }] as any)
   vi.mocked(getBudgetCategories).mockResolvedValue([{ category_id: 'cat1', name: 'Essen & Haushalt', category_type: 'expense', is_active: true, children: [] }, { category_id: 'cat_media', name: 'Elektronische Medien', category_type: 'expense', is_active: true, children: [] }] as any)
   vi.mocked(getBudgetTransactionCandidateSummary).mockResolvedValue({ open_total: 2, auto_categorized: 0, review_needed: 1, galaxus_digitec: 0, migros_over_50: 0, covered_by_migros: 0, unclear: 0, possible_duplicates: 1 } as any)
   vi.mocked(getBudgetTransactionCandidates).mockResolvedValue([


  vi.mocked(confirmBudgetTransactionCandidateDuplicateOverride).mockResolvedValue({ status: 'confirmed', entity_id: 'tx_dup', audit_id: 'audit_dup', message: 'Duplikat-Override bestätigt' } as any)
  vi.mocked(confirmBudgetTransactionCandidate).mockResolvedValue({ status: 'confirmed', entity_id: 'transfer1', audit_id: 'audit_transfer', message: 'Transfer bestätigt' } as any)
})

  vi.mocked(confirmBudgetTransactionCandidateDuplicateOverride).mockResolvedValue({ status: 'confirmed', entity_id: 'tx_dup', audit_id: 'audit_dup', message: 'Duplikat-Override bestätigt' } as any)
})


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
@@ -34,6 +34,7 @@
   vi.mocked(batchBudgetTransactionCandidates).mockResolvedValue({ updated_count: 2, skipped_count: 0, confirmed_count: 2, ignored_count: 2, reopened_count: 2, audit_id: 'audit_batch', errors: [] } as any)
   vi.mocked(previewBatchBudgetTransactionCandidates).mockResolvedValue({ preview_id: 'batchprev1', count: 1, categories: { 'Elektronische Medien': 1 }, sources: { credit_card_csv: 1 }, possible_duplicates: 1, blockers: { duplicate_candidates: 1, covered_by_source: 0, transfer_candidates: 0, without_category: 0, missing_required_fields: 0, not_confirmable: 0 }, conflicts: [{ candidate_id: 'cand2', reason: 'duplicate_candidate_blocked' }], can_confirm: false, requires_explicit_confirm: true } as any)
   vi.mocked(confirmBudgetTransactionCandidateDuplicateOverride).mockResolvedValue({ status: 'confirmed', entity_id: 'tx_dup', audit_id: 'audit_dup', message: 'Duplikat-Override bestätigt' } as any)
+  vi.mocked(confirmBudgetTransactionCandidate).mockResolvedValue({ status: 'confirmed', entity_id: 'transfer1', audit_id: 'audit_transfer', message: 'Transfer bestätigt' } as any)
 })

 describe('Budget Import Production v2 review UI', () => {


  it('renders internal transfers separately and confirms them as transfer payloads only', async () => {
    vi.mocked(getBudgetTransactionCandidates).mockResolvedValue([
      { transaction_candidate_id: 'cand_transfer', source_file_label: 'raiffeisen.csv', source_type: 'raiffeisen_bank', transaction_date: '2026-01-26', description: 'Einkauf TWINT SWISSLOS E-COMMERCE', amount_original: '100.00', currency_original: 'CHF', confidence: '0.96', requires_review: true, status: 'transfer_candidate', classification: 'transfer_candidate', review_reason: 'swisslos_raiffeisen_akb_internal_transfer', rule_name: 'Raiffeisen TWINT Swisslos → AKB Haushaltskonto Transfer', account_source: 'Raiffeisen', transfer_target_account_name: 'AKB Haushaltskonto', transfer_pair_status: 'unmatched', budget_impact: 'neutral' }
    ] as any)
    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    await flushPromises()

    expect(wrapper.text()).toContain('Interne Transfers')
    expect(wrapper.text()).toContain('AKB Haushaltskonto')
    expect(wrapper.text()).toContain('Gegenbuchung nicht gefunden')
    expect(wrapper.text()).toContain('als Transfer bestätigen')
    expect(wrapper.text()).not.toContain('Review nötig')

    ;(wrapper.vm as any).confirmAccount = 'acc1'
    ;(wrapper.vm as any).transferTargetAccount = 'acc_akb'
    await (wrapper.vm as any).confirmTransferItem({ transaction_candidate_id: 'cand_transfer', status: 'transfer_candidate', classification: 'transfer_candidate', transaction_date: '2026-01-26' })
    await flushPromises()
    expect(confirmBudgetTransactionCandidate).toHaveBeenCalledWith('cand_transfer', { from_account_id: 'acc1', to_account_id: 'acc_akb', transaction_type: 'transfer', budget_year: '2026' })
  })
})


Found 17 matches for old_string. Provide more context to make it unique, or use replace_all=True.


  it('requires a reason and calls the duplicate override endpoint explicitly', async () => {
    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    await flushPromises()
    ;(wrapper.vm as any).confirmAccount = 'acc1'
    ;(wrapper.vm as any).openDuplicateOverride({ transaction_candidate_id: 'cand2', status: 'duplicate_candidate', transaction_date: '2026-03-03', proposed_category_id: 'cat1', description: 'GOOGLE STORAGE', merchant: 'Google' })
    await flushPromises()
    ;(wrapper.vm as any).duplicateOverrideReason = 'Bewusst separater Einkauf trotz Treffer'
    await (wrapper.vm as any).confirmDuplicateOverride()
    await flushPromises()

    expect(confirmBudgetTransactionCandidateDuplicateOverride).toHaveBeenCalledWith('cand2', { account_id: 'acc1', category_id: 'cat1', transaction_type: 'expense', budget_year: '2026', reason: 'Bewusst separater Einkauf trotz Treffer' })
  })

  it('renders internal transfers separately and confirms them as transfer payloads only', async () => {
    vi.mocked(getBudgetTransactionCandidates).mockResolvedValue([
      { transaction_candidate_id: 'cand_transfer', source_file_label: 'raiffeisen.csv', source_type: 'raiffeisen_bank', transaction_date: '2026-01-26', description: 'Einkauf TWINT SWISSLOS E-COMMERCE', amount_original: '100.00', currency_original: 'CHF', confidence: '0.96', requires_review: true, status: 'transfer_candidate', classification: 'transfer_candidate', review_reason: 'swisslos_raiffeisen_akb_internal_transfer', rule_name: 'Raiffeisen TWINT Swisslos → AKB Haushaltskonto Transfer', account_source: 'Raiffeisen', transfer_target_account_name: 'AKB Haushaltskonto', transfer_pair_status: 'unmatched', budget_impact: 'neutral' }
    ] as any)
    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    await flushPromises()

    expect(wrapper.text()).toContain('Interne Transfers')
    expect(wrapper.text()).toContain('AKB Haushaltskonto')
    expect(wrapper.text()).toContain('Gegenbuchung nicht gefunden')
    expect(wrapper.text()).toContain('als Transfer bestätigen')

    ;(wrapper.vm as any).confirmAccount = 'acc1'
    ;(wrapper.vm as any).transferTargetAccount = 'acc_akb'
    await (wrapper.vm as any).confirmTransferItem({ transaction_candidate_id: 'cand_transfer', status: 'transfer_candidate', classification: 'transfer_candidate', transaction_date: '2026-01-26' })
    await flushPromises()
    expect(confirmBudgetTransactionCandidate).toHaveBeenCalledWith('cand_transfer', { from_account_id: 'acc1', to_account_id: 'acc_akb', transaction_type: 'transfer', budget_year: '2026' })
  })
})

  it('requires a reason and calls the duplicate override endpoint explicitly', async () => {
    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
    await flushPromises()
    ;(wrapper.vm as any).confirmAccount = 'acc1'
    ;(wrapper.vm as any).openDuplicateOverride({ transaction_candidate_id: 'cand2', status: 'duplicate_candidate', transaction_date: '2026-03-03', proposed_category_id: 'cat1', description: 'GOOGLE STORAGE', merchant: 'Google' })
    await flushPromises()
    ;(wrapper.vm as any).duplicateOverrideReason = 'Bewusst separater Einkauf trotz Treffer'
    await (wrapper.vm as any).confirmDuplicateOverride()
    await flushPromises()

    expect(confirmBudgetTransactionCandidateDuplicateOverride).toHaveBeenCalledWith('cand2', { account_id: 'acc1', category_id: 'cat1', transaction_type: 'expense', budget_year: '2026', reason: 'Bewusst separater Einkauf trotz Treffer' })
  })
})


--- a//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
+++ b//home/agent/.hermes/repos/FinanceManager/frontend/src/pages/BudgetImportProductionV2.test.ts
@@ -116,4 +116,23 @@

     expect(confirmBudgetTransactionCandidateDuplicateOverride).toHaveBeenCalledWith('cand2', { account_id: 'acc1', category_id: 'cat1', transaction_type: 'expense', budget_year: '2026', reason: 'Bewusst separater Einkauf trotz Treffer' })
   })
+
+  it('renders internal transfers separately and confirms them as transfer payloads only', async () => {
+    vi.mocked(getBudgetTransactionCandidates).mockResolvedValue([
+      { transaction_candidate_id: 'cand_transfer', source_file_label: 'raiffeisen.csv', source_type: 'raiffeisen_bank', transaction_date: '2026-01-26', description: 'Einkauf TWINT SWISSLOS E-COMMERCE', amount_original: '100.00', currency_original: 'CHF', confidence: '0.96', requires_review: true, status: 'transfer_candidate', classification: 'transfer_candidate', review_reason: 'swisslos_raiffeisen_akb_internal_transfer', rule_name: 'Raiffeisen TWINT Swisslos → AKB Haushaltskonto Transfer', account_source: 'Raiffeisen', transfer_target_account_name: 'AKB Haushaltskonto', transfer_pair_status: 'unmatched', budget_impact: 'neutral' }
+    ] as any)
+    const wrapper = mount(BudgetImportReviewPage, { global: { stubs: { teleport: true } } })
+    await flushPromises()
+
+    expect(wrapper.text()).toContain('Interne Transfers')
+    expect(wrapper.text()).toContain('AKB Haushaltskonto')
+    expect(wrapper.text()).toContain('Gegenbuchung nicht gefunden')
+    expect(wrapper.text()).toContain('als Transfer bestätigen')
+
+    ;(wrapper.vm as any).confirmAccount = 'acc1'
+    ;(wrapper.vm as any).transferTargetAccount = 'acc_akb'
+    await (wrapper.vm as any).confirmTransferItem({ transaction_candidate_id: 'cand_transfer', status: 'transfer_candidate', classification: 'transfer_candidate', transaction_date: '2026-01-26' })
+    await flushPromises()
+    expect(confirmBudgetTransactionCandidate).toHaveBeenCalledWith('cand_transfer', { from_account_id: 'acc1', to_account_id: 'acc_akb', transaction_type: 'transfer', budget_year: '2026' })
+  })
 })


terminal

PYTHONPATH=src pytest tests/unit/test_budget_internal_transfer_hotfix.py -q && cd frontend && npm test -- --run src/pages/BudgetImportProductionV2.test.ts


Output:
...                                                                      [100%]
3 passed in 0.45s

> jarvis-finance-dashboard-vue@0.0.0 test
> vitest run --environment jsdom --run src/pages/BudgetImportProductionV2.test.ts

 RUN  v4.1.6 /home/agent/.hermes/repos/FinanceManager/frontend

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > shows source-aware review tabs and candidate type actions
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > marks a candidate as credit card payment through audited type action
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > shows bulk toolbar state, applies category to selected candidates, and keeps partial feedback visible
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > opens bulk confirm preview dialog and disables final confirm when blockers exist
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > keeps duplicate candidates out of normal confirm actions and shows duplicate review actions only
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > requires a reason and calls the duplicate override endpoint explicitly
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

stderr | src/pages/BudgetImportProductionV2.test.ts > Budget Import Production v2 review UI > renders internal transfers separately and confirms them as transfer payloads only
[Vue warn]: injection "Symbol(router)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>
[Vue warn]: injection "Symbol(route location)" not found.
  at <BudgetImportReviewPage ref="VTU_COMPONENT" >
  at <VTUROOT>

 ✓ src/pages/BudgetImportProductionV2.test.ts (7 tests) 3289ms
     ✓ shows source-aware review tabs and candidate type actions  594ms
     ✓ marks a candidate as credit card payment through audited type action  431ms
     ✓ shows bulk toolbar state, applies category to selected candidates, and keeps partial feedback visible  433ms
     ✓ opens bulk confirm preview dialog and disables final confirm when blockers exist  430ms
     ✓ keeps duplicate candidates out of normal confirm actions and shows duplicate review actions only  393ms
     ✓ requires a reason and calls the duplicate override endpoint explicitly  445ms
     ✓ renders internal transfers separately and confirms them as transfer payloads only  562ms

 Test Files  1 passed (1)
      Tests  7 passed (7)
   Start at  21:26:23
   Duration  4.59s (transform 246ms, setup 148ms, import 352ms, tests 3.29s, environment 687ms)


Explanation:
**Inspecting backup requirements**

I need to focus on runtime corrections while also ensuring I have a backup and audit process in place. I might need to check the existing candidate counts and apply an endpoint or service before the runtime backup. It’s essential to create a rule in budget_rules or merchant aliases, especially since the user asked about saving user rules or aliases. I should also examine the schema for budget_rules to make sure everything aligns properly.

python - <<'PY'
import sqlite3
con=sqlite3.connect('/home/agent/jarvis_runtime/finance-system/data/finance.sqlite3')
for t in ['budget_rules','merchant_aliases','merchants','budget_transaction_candidates']:
 print('\n',t)
 for r in con.execute(f'PRAGMA table_info({t})'):
  print(r)
PY


Output:
budget_rules

 merchant_aliases

 merchants

 budget_transaction_candidates
(0, 'transaction_candidate_id', 'TEXT', 0, None, 1)
(1, 'source_file_label', 'TEXT', 1, None, 0)
(2, 'source_row_or_range', 'TEXT', 0, None, 0)
(3, 'source_type', 'TEXT', 1, "'csv_seed'", 0)
(4, 'transaction_date', 'TEXT', 0, None, 0)
(5, 'description', 'TEXT', 1, None, 0)
(6, 'merchant', 'TEXT', 0, None, 0)
(7, 'amount_original', 'TEXT', 0, None, 0)
(8, 'currency_original', 'TEXT', 1, "'CHF'", 0)
(9, 'proposed_category_id', 'TEXT', 0, None, 0)
(10, 'proposed_category_name', 'TEXT', 0, None, 0)
(11, 'duplicate_of_transaction_id', 'TEXT', 0, None, 0)
(12, 'confidence', 'TEXT', 1, "'0'", 0)
(13, 'requires_review', 'INTEGER', 1, '1', 0)
(14, 'status', 'TEXT', 1, "'pending'", 0)
(15, 'notes', 'TEXT', 0, None, 0)
(16, 'created_at', 'TEXT', 1, None, 0)
(17, 'updated_at', 'TEXT', 0, None, 0)
(18, 'classification', 'TEXT', 0, None, 0)
(19, 'review_reason', 'TEXT', 0, None, 0)
(20, 'rule_id', 'TEXT', 0, None, 0)
(21, 'rule_name', 'TEXT', 0, None, 0)
(22, 'source_priority', 'INTEGER', 1, '50', 0)
(23, 'covered_by_source', 'TEXT', 0, None, 0)
(24, 'receipt_key', 'TEXT', 0, None, 0)
(25, 'linked_candidate_id', 'TEXT', 0, None, 0)
(26, 'account_source', 'TEXT', 0, None, 0)
(27, 'raw_fingerprint', 'TEXT', 0, None, 0)
(28, 'confirmed_transaction_id', 'TEXT', 0, None, 0)
(29, 'confirmed_at', 'TEXT', 0, None, 0)
(30, 'confirmed_by', 'TEXT', 0, None, 0)
(31, 'merchant_id', 'TEXT', 0, None, 0)
(32, 'merchant_display_name', 'TEXT', 0, None, 0)
(33, 'status_label', 'TEXT', 0, None, 0)
(34, 'import_session_id', 'TEXT', 0, None, 0)


Explanation:
**Checking budget rules**

I need to look into why there don't seem to be any budget rules. Perhaps the migrations weren't applied to the runtime? It looks like I should make a list of tables to better understand the situation and see what's going on. I'll dig deeper and gather the necessary information to help clarify things. Let's ensure everything is set up correctly!

python - <<'PY'
import sqlite3
con=sqlite3.connect('/home/agent/jarvis_runtime/finance-system/data/finance.sqlite3')
for r in con.execute("select name from sqlite_master where type='table' and name like '%rule%' or name like '%merchant%' order by name"):
 print(r[0])
PY


Output:
budget_import_rules
budget_merchant_aliases
budget_merchants
budget_review_rules
budget_rule_suggestions
idx_budget_merchant_aliases_active
idx_budget_merchant_aliases_merchant
idx_budget_merchants_active
idx_budget_merchants_normalized
idx_budget_recurring_merchant
sqlite_autoindex_budget_merchant_aliases_1
sqlite_autoindex_budget_merchants_1


python - <<'PY'
import sqlite3
con=sqlite3.connect('/home/agent/jarvis_runtime/finance-system/data/finance.sqlite3')
for t in ['budget_import_rules','budget_review_rules','budget_merchants','budget_merchant_aliases']:
 print('\n', t)
 for r in con.execute(f'PRAGMA table_info({t})'):
  print(r)
PY


Output:
budget_import_rules
(0, 'rule_id', 'TEXT', 0, None, 1)
(1, 'rule_type', 'TEXT', 1, None, 0)
(2, 'pattern', 'TEXT', 1, None, 0)
(3, 'source_type', 'TEXT', 0, None, 0)
(4, 'target_action', 'TEXT', 1, None, 0)
(5, 'target_category_id', 'TEXT', 0, None, 0)
(6, 'threshold_amount', 'TEXT', 0, None, 0)
(7, 'confidence', 'TEXT', 1, "'0.80'", 0)
(8, 'is_active', 'INTEGER', 1, '1', 0)
(9, 'notes', 'TEXT', 0, None, 0)
(10, 'created_at', 'TEXT', 1, None, 0)
(11, 'updated_at', 'TEXT', 0, None, 0)

 budget_review_rules
(0, 'rule_id', 'TEXT', 0, None, 1)
(1, 'merchant_contains', 'TEXT', 1, None, 0)
(2, 'source_type', 'TEXT', 0, None, 0)
(3, 'category_id', 'TEXT', 0, None, 0)
(4, 'target_status', 'TEXT', 1, "'review'", 0)
(5, 'is_active', 'INTEGER', 1, '1', 0)
(6, 'created_at', 'TEXT', 1, None, 0)
(7, 'updated_at', 'TEXT', 0, None, 0)
(8, 'priority', 'INTEGER', 1, '100', 0)
(9, 'confidence', 'TEXT', 1, "'0.82'", 0)
(10, 'notes', 'TEXT', 0, None, 0)

 budget_merchants
(0, 'merchant_id', 'TEXT', 0, None, 1)
(1, 'display_name', 'TEXT', 1, None, 0)
(2, 'normalized_name', 'TEXT', 1, None, 0)
(3, 'default_category_id', 'TEXT', 0, None, 0)
(4, 'is_active', 'INTEGER', 1, '1', 0)
(5, 'notes', 'TEXT', 0, None, 0)
(6, 'created_at', 'TEXT', 1, None, 0)
(7, 'updated_at', 'TEXT', 0, None, 0)

 budget_merchant_aliases
(0, 'alias_id', 'TEXT', 0, None, 1)
(1, 'merchant_id', 'TEXT', 1, None, 0)
(2, 'pattern', 'TEXT', 1, None, 0)
(3, 'match_type', 'TEXT', 1, "'contains'", 0)
(4, 'source_type', 'TEXT', 0, None, 0)
(5, 'priority', 'INTEGER', 1, '100', 0)
(6, 'is_active', 'INTEGER', 1, '1', 0)
(7, 'created_at', 'TEXT', 1, None, 0)
(8, 'updated_at', 'TEXT', 0, None, 0)


Explanation:
**Considering budget rules**

I’m thinking I might need to add a persistent rule for budget_import_rules. To do this, I’ll create a function called ensure_rule. I can implement this function inside classify_swisslos_internal_transfers to create or update the import rule. I’ll also need to ensure I have tests for the rule that is stored. Plus, I should check on the memory database migrations to see if it already has budget_import_rules in place. Adding the relevant code will be the next step!