// Domain types for the API responses. Money fields are branded `Dollars` — the // server stores integer cents and serializes dollars over the wire (via // utils/money.fromCents), so every amount a component receives is dollars. Typing // them `Dollars` lets `fmt`/`formatUSD` accept them while rejecting raw cents // (e.g. bank-transaction `*_cents` fields), catching the cents-as-dollars bug // class at compile time. // // Shapes are typed incrementally (endpoint-by-endpoint). Complex nested payloads // that no typed consumer reads yet (trend series, autopay suggestion/stats, // sparkline) are left `unknown` and refined when a consumer needs them. import type { Cents, Dollars } from '@/lib/money'; // A bank transaction as serialized by the server. Unlike bill/payment amounts, // raw bank-transaction amounts stay in CENTS on the wire — hence branded `Cents` // (format them with formatCentsUSD, never formatUSD). export interface BankTransaction { id: number; amount: Cents; currency?: string | null; payee?: string | null; description?: string | null; memo?: string | null; posted_date?: string | null; transacted_at?: string | number | null; account_name?: string | null; source_label?: string | null; source_type_label?: string | null; [key: string]: unknown; } // The bank ledger (GET /transactions/bank-ledger): accounts, the transaction // occurrence with spending/match metadata, and the month summary. Raw // transaction/account amounts stay in CENTS on this endpoint (the page formats // them with a local formatCents), so they're plain `number` cents, not Dollars. export interface BankLedgerTransaction extends BankTransaction { transacted_at?: string | null; // narrower than BankTransaction's for date slicing category?: string | null; pending?: boolean; ignored?: boolean; match_status?: string | null; matched_bill_name?: string | null; spending_category_id?: number | null; spending_category_name?: string | null; suggested_match?: { display_name?: string; category?: string } | null; account_org_name?: string | null; account_type?: string | null; } export interface BankAccount { id: number | string; org_name?: string; name?: string; account_type?: string; currency?: string; balance?: number; available_balance?: number | null; transaction_count?: number; last_transaction_date?: string; } export interface BankCategoryBreakdown { name: string; total?: number; count?: number } export interface BankLedgerSummary { inflow?: number; outflow?: number; net?: number; matched?: number; unmatched?: number; pending?: number; total?: number; latest_date?: string; category_breakdown?: BankCategoryBreakdown[]; } export interface BankLedger { accounts?: BankAccount[]; transactions?: BankLedgerTransaction[]; summary?: BankLedgerSummary; total?: number; sources?: { last_sync_at?: string | null }[]; enabled?: boolean; has_connections?: boolean; } export interface AutoCategorizeChange { transaction_id: number } export interface AutoCategorizePreview { changes?: AutoCategorizeChange[]; categories?: { name: string; count?: number }[]; } // A bill's month status. Mirrors the server's statusService. export type TrackerStatus = | 'paid' | 'autodraft' | 'upcoming' | 'due_soon' | 'late' | 'missed' | 'skipped'; // A price-drift finding (driftService.getDriftReport) — a bill whose recent // payments trend away from its expected amount. export interface DriftBill { id: number; name: string; category_name?: string | null; expected_amount: Dollars; recent_amount: Dollars; direction: string; drift_pct: number; months_sampled: number; } // A suggested autopay payment (trackerService.applyAutopaySuggestions). export interface AutopaySuggestion { bill_id: number; amount: Dollars; paid_date: string; method: string; } // A suggested expected-amount for a bill, from its recent payment history. export interface AmountSuggestion { suggestion: Dollars; months_used: number; } // Autopay reliability rollup shown in the tracker-row tooltip. export interface AutopayStats { total: number; failures: number; last_failure_date?: string | null; last_failure_notes?: string | null; } // A payment record as serialized by the server (money in dollars). serializePayment // spreads the full DB row, so extra columns pass through as `unknown`. export interface Payment { id: number; bill_id?: number; amount: Dollars; paid_date: string; method?: string | null; payment_source?: string | null; notes?: string | null; autopay_failure?: number | null; balance_delta?: Dollars | null; interest_delta?: Dollars | null; created_at?: string; deleted_at?: string | null; [key: string]: unknown; } // A category's per-bill rollup (categories list endpoint). `total_paid` here is a // raw SQL sum (not run through fromCents), so it is deliberately NOT branded. export interface CategoryBillSummary { id: number; name: string; active: boolean; payment_count: number; total_paid: number; last_paid_date: string | null; [key: string]: unknown; } // A spending/bill category. Has no money field of its own (budgets live on the // spending endpoints). The list endpoint adds bill rollups; create/update return // the bare row — so the rollup fields are optional. export interface Category { id: number; user_id?: number; name: string; sort_order?: number; spending_enabled: boolean; group_id: number | null; created_at?: string; updated_at?: string; bill_count?: number; active_bill_count?: number; inactive_bill_count?: number; payment_count?: number; bill_names?: string[]; bills?: CategoryBillSummary[]; [key: string]: unknown; } // A bill as serialized by the server (billsService.serializeBill spreads the DB // row and converts the three money columns to dollars). SQLite booleans come back // as 0/1 integers. Extra joined columns (sparkline, has_merchant_rule, …) pass // through as `unknown` via the index signature. export interface Bill { id: number; user_id?: number; name: string; category_id: number | null; category_name?: string | null; due_day: number | null; override_due_date: string | null; bucket: string; expected_amount: Dollars; interest_rate: number | null; billing_cycle: string | null; autopay_enabled: number; autodraft_status: string | null; auto_mark_paid: number; website: string | null; username: string | null; account_info: string | null; has_2fa: number; notes: string | null; history_visibility: string | null; active: number; cycle_type: string | null; cycle_day: string | number | null; current_balance: Dollars | null; minimum_payment: Dollars | null; snowball_order: number | null; snowball_include: number; snowball_exempt: number; is_subscription: number; subscription_type: string | null; reminder_days_before: number | null; subscription_source: string | null; subscription_detected_at: string | null; created_at?: string; updated_at?: string; deleted_at?: string | null; [key: string]: unknown; } // One tracker row: a bill's state for a given month. Built by // statusService.buildTrackerRow and augmented by trackerService.getTracker. export interface TrackerRow { id: number; name: string; category_id: number | null; category_name: string | null; due_date: string; due_day: number | null; bucket: string; expected_amount: Dollars; notes: string | null; total_paid: Dollars; paid_toward_due: Dollars; overpaid_amount: Dollars; balance: Dollars; has_payment: boolean; is_settled: boolean; last_paid_date: string | null; last_payment_amount: Dollars | null; status: TrackerStatus; autopay_enabled: boolean; autodraft_status: string | null; auto_mark_paid: boolean; billing_cycle: string | null; cycle_type: string | null; cycle_day: string | number | null; current_balance: Dollars | null; minimum_payment: Dollars | null; interest_rate: number | null; is_subscription: boolean; has_2fa: boolean; has_merchant_rule: boolean; has_linked_transactions: boolean; website: string | null; autopay_verified_at: string | null; inactive_reason: string | null; inactivated_at: string | null; sparkline: number[] | null; autopay_stats: AutopayStats | null; payments: Payment[]; // Augmented by getTracker: actual_amount: Dollars | null; is_skipped: boolean; snoozed_until: string | null; monthly_notes?: string | null; autopay_suggestion?: AutopaySuggestion | null; amount_suggestion?: AmountSuggestion | null; previous_month_paid: Dollars; // Bank-mode augmentation (present only when bank_tracking.enabled): pending_cleared?: boolean; bank_pending_count?: number; } export interface TrackerSummary { total_expected: Dollars; total_starting: Dollars; has_starting_amounts: boolean; total_paid: Dollars; paid_toward_due: Dollars; remaining: Dollars; total_remaining: Dollars; remaining_period: string; remaining_label: string; remaining_hint: string; overdue: Dollars; count_paid: number; count_upcoming: number; count_late: number; count_autodraft: number; previous_month_total: Dollars; trend: unknown; } // Bank tracking is off (`{ enabled: false }`) or on with the account snapshot. export type BankTracking = | { enabled: false } | { enabled: true; account_id: number; account_name: string; org_name: string; balance: Dollars; pending_payments: Dollars; pending_days: number; effective_balance: Dollars; unpaid_this_month: Dollars; remaining: Dollars; last_updated: string | null; }; export interface SafeToSpendUpcoming { id: number; name: string; due_date: string; amount: Dollars; status: TrackerStatus; } // A bill landing on a given cashflow-timeline day. export interface TimelineBill { name: string; amount: Dollars; } export interface CashflowTimelinePoint { date: string; balance: number; // running dollar balance; cashflowUtils treats it loosely bills: TimelineBill[]; payday?: boolean; } export interface TrackerCashflow { next_payday: string | null; days_until_payday: number; available: Dollars; safe_to_spend: Dollars; still_due_total: Dollars; still_due_count: number; upcoming: SafeToSpendUpcoming[]; timeline: CashflowTimelinePoint[]; has_data: boolean; uses_bank_balance: boolean; period: string; period_end_label: string; period_starting: Dollars; period_bills_total: Dollars; period_paid: Dollars; period_paid_count: number; period_total_count: number; period_projected: Dollars; month_starting: Dollars; month_bills_total: Dollars; month_paid: Dollars; month_paid_count: number; month_total_count: number; month_projected: Dollars; } export interface TrackerResponse { year: number; month: number; today: string; summary: TrackerSummary; bank_tracking: BankTracking; cashflow: TrackerCashflow; rows: TrackerRow[]; }