BillTracker/client/types.ts

407 lines
12 KiB
TypeScript
Raw Normal View History

// 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 }[];
}
// The spending page (GET /spending/*). Amounts are dollars (server fromCents);
// kept as plain `number` because the page works in numbers via a local formatter.
export interface SpendingCategoryEntry {
category_id: number | 'other' | null;
category_name?: string;
amount: number;
budget?: number | null;
tx_count?: number;
avg_3mo?: number;
group_id?: number | null;
}
export interface SpendingSummary {
by_category?: SpendingCategoryEntry[];
total_spending?: number;
income?: number;
uncategorized_amount?: number;
uncategorized_count?: number;
}
export interface SpendingTransaction {
id: number;
payee?: string | null;
date?: string | null;
amount: number;
spending_category_id?: number | string | null;
spending_category_name?: string | null;
[key: string]: unknown;
}
export interface SpendingTransactionsResponse { transactions?: SpendingTransaction[]; total?: number; pages?: number }
export interface SpendingIncomeTx { id: number; payee?: string | null; date?: string | null; amount: number }
export interface SpendingIncomeResponse { transactions?: SpendingIncomeTx[]; total?: number; pages?: number }
export interface SpendingRule { id: number; merchant: string; category_name?: string; category_id?: number }
export interface CategoryGroup { id: number; name: string }
export interface CopyBudgetsResponse { copied: number; budgets?: { category_id: number; amount: 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[];
}