Files
outlay/outlay-core/src/models.rs
lashman 10a76e3003 Add feature batch 2, subscription/recurring sync, smooth charts, and app icon
- Implement subscriptions view with bidirectional recurring transaction sync
- Add cascade delete/pause/resume between subscriptions and recurring
- Fix foreign key constraints when deleting recurring transactions
- Add cross-view instant refresh via callback pattern
- Replace Bezier chart smoothing with Fritsch-Carlson monotone Hermite interpolation
- Smooth budget sparklines using shared monotone_subdivide function
- Add vertical spacing to budget rows
- Add app icon (receipt on GNOME blue) in all sizes for desktop, web, and AppImage
- Add calendar, credit cards, forecast, goals, insights, and wishlist views
- Add date picker, numpad, quick-add, category combo, and edit dialog components
- Add import/export for CSV, JSON, OFX, QIF formats
- Add NLP transaction parsing, OCR receipt scanning, expression evaluator
- Add notification support, Sankey chart, tray icon
- Add demo data seeder with full DB wipe
- Expand database schema with subscriptions, goals, credit cards, and more
2026-03-03 21:18:37 +02:00

457 lines
11 KiB
Rust

use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransactionType {
Expense,
Income,
}
impl TransactionType {
pub fn as_str(&self) -> &'static str {
match self {
TransactionType::Expense => "expense",
TransactionType::Income => "income",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"expense" => Some(TransactionType::Expense),
"income" => Some(TransactionType::Income),
_ => None,
}
}
}
impl fmt::Display for TransactionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Frequency {
Daily,
Weekly,
Biweekly,
Monthly,
Yearly,
}
impl Frequency {
pub fn as_str(&self) -> &'static str {
match self {
Frequency::Daily => "daily",
Frequency::Weekly => "weekly",
Frequency::Biweekly => "biweekly",
Frequency::Monthly => "monthly",
Frequency::Yearly => "yearly",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"daily" => Some(Frequency::Daily),
"weekly" => Some(Frequency::Weekly),
"biweekly" => Some(Frequency::Biweekly),
"monthly" => Some(Frequency::Monthly),
"yearly" => Some(Frequency::Yearly),
_ => None,
}
}
}
impl fmt::Display for Frequency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Category {
pub id: i64,
pub name: String,
pub icon: Option<String>,
pub color: Option<String>,
pub transaction_type: TransactionType,
pub is_default: bool,
pub sort_order: i32,
pub parent_id: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct NewCategory {
pub name: String,
pub icon: Option<String>,
pub color: Option<String>,
pub transaction_type: TransactionType,
pub sort_order: i32,
pub parent_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Transaction {
pub id: i64,
pub amount: f64,
pub transaction_type: TransactionType,
pub category_id: i64,
pub currency: String,
pub exchange_rate: f64,
pub note: Option<String>,
pub date: NaiveDate,
pub created_at: String,
pub recurring_id: Option<i64>,
pub payee: Option<String>,
}
#[derive(Debug, Clone)]
pub struct NewTransaction {
pub amount: f64,
pub transaction_type: TransactionType,
pub category_id: i64,
pub currency: String,
pub exchange_rate: f64,
pub note: Option<String>,
pub date: NaiveDate,
pub recurring_id: Option<i64>,
pub payee: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Budget {
pub id: i64,
pub category_id: i64,
pub amount: f64,
pub month: String,
pub rollover: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecurringTransaction {
pub id: i64,
pub amount: f64,
pub transaction_type: TransactionType,
pub category_id: i64,
pub currency: String,
pub note: Option<String>,
pub frequency: Frequency,
pub start_date: NaiveDate,
pub end_date: Option<NaiveDate>,
pub last_generated: Option<NaiveDate>,
pub active: bool,
pub resume_date: Option<NaiveDate>,
pub is_bill: bool,
pub reminder_days: i32,
pub subscription_id: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct NewRecurringTransaction {
pub amount: f64,
pub transaction_type: TransactionType,
pub category_id: i64,
pub currency: String,
pub note: Option<String>,
pub frequency: Frequency,
pub start_date: NaiveDate,
pub end_date: Option<NaiveDate>,
pub is_bill: bool,
pub reminder_days: i32,
pub subscription_id: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeRate {
pub base: String,
pub target: String,
pub rate: f64,
pub fetched_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
pub id: i64,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Split {
pub id: i64,
pub transaction_id: i64,
pub category_id: i64,
pub amount: f64,
pub note: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionTemplate {
pub id: i64,
pub name: String,
pub amount: Option<f64>,
pub transaction_type: TransactionType,
pub category_id: i64,
pub currency: String,
pub payee: Option<String>,
pub note: Option<String>,
pub tags: Option<String>,
pub sort_order: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategorizeRule {
pub id: i64,
pub field: String,
pub pattern: String,
pub category_id: i64,
pub priority: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavingsGoal {
pub id: i64,
pub name: String,
pub target: f64,
pub saved: f64,
pub currency: String,
pub deadline: Option<NaiveDate>,
pub color: Option<String>,
pub icon: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WishlistItem {
pub id: i64,
pub name: String,
pub amount: f64,
pub category_id: Option<i64>,
pub url: Option<String>,
pub note: Option<String>,
pub priority: i32,
pub purchased: bool,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionCategory {
pub id: i64,
pub name: String,
pub icon: Option<String>,
pub color: Option<String>,
pub sort_order: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subscription {
pub id: i64,
pub name: String,
pub amount: f64,
pub currency: String,
pub frequency: Frequency,
pub category_id: i64,
pub start_date: NaiveDate,
pub next_due: NaiveDate,
pub active: bool,
pub note: Option<String>,
pub url: Option<String>,
pub recurring_id: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct NewSubscription {
pub name: String,
pub amount: f64,
pub currency: String,
pub frequency: Frequency,
pub category_id: i64,
pub start_date: NaiveDate,
pub note: Option<String>,
pub url: Option<String>,
pub recurring_id: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct CreditCard {
pub id: i64,
pub name: String,
pub credit_limit: Option<f64>,
pub statement_close_day: i32,
pub due_day: i32,
pub min_payment_pct: f64,
pub current_balance: f64,
pub currency: String,
pub color: Option<String>,
pub active: bool,
}
#[derive(Debug, Clone)]
pub struct NewCreditCard {
pub name: String,
pub credit_limit: Option<f64>,
pub statement_close_day: i32,
pub due_day: i32,
pub min_payment_pct: f64,
pub currency: String,
pub color: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Achievement {
pub id: i64,
pub name: String,
pub description: String,
pub earned_at: Option<String>,
pub icon: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ParsedTransaction {
pub amount: f64,
pub category_name: Option<String>,
pub category_id: Option<i64>,
pub note: Option<String>,
pub payee: Option<String>,
pub transaction_type: TransactionType,
}
#[derive(Debug, Clone)]
pub struct SankeyNode {
pub label: String,
pub value: f64,
pub color: (f64, f64, f64),
pub y: f64,
pub height: f64,
}
#[derive(Debug, Clone)]
pub struct SankeyFlow {
pub from_idx: usize,
pub to_idx: usize,
pub value: f64,
pub from_y: f64,
pub to_y: f64,
pub width: f64,
}
#[derive(Debug, Clone)]
pub struct SankeyLayout {
pub left_nodes: Vec<SankeyNode>,
pub right_nodes: Vec<SankeyNode>,
pub center_y: f64,
pub center_height: f64,
pub flows_in: Vec<SankeyFlow>,
pub flows_out: Vec<SankeyFlow>,
pub net: f64,
}
#[derive(Debug, Clone)]
pub struct RecapCategory {
pub category_name: String,
pub category_icon: Option<String>,
pub category_color: Option<String>,
pub amount: f64,
pub percentage: f64,
pub change_pct: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct MonthlyRecap {
pub total_income: f64,
pub total_expenses: f64,
pub net: f64,
pub transaction_count: i64,
pub categories: Vec<RecapCategory>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BudgetCycleMode {
Calendar,
Payday,
Rolling,
}
impl BudgetCycleMode {
pub fn as_str(&self) -> &'static str {
match self {
BudgetCycleMode::Calendar => "calendar",
BudgetCycleMode::Payday => "payday",
BudgetCycleMode::Rolling => "rolling",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"payday" => BudgetCycleMode::Payday,
"rolling" => BudgetCycleMode::Rolling,
_ => BudgetCycleMode::Calendar,
}
}
}
impl fmt::Display for BudgetCycleMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct PdfParsedRow {
pub date: Option<NaiveDate>,
pub description: String,
pub amount: f64,
pub is_credit: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transaction_type_round_trip() {
for tt in [TransactionType::Expense, TransactionType::Income] {
let s = tt.as_str();
let parsed = TransactionType::from_str(s).unwrap();
assert_eq!(tt, parsed);
}
}
#[test]
fn test_transaction_type_invalid() {
assert_eq!(TransactionType::from_str("bogus"), None);
}
#[test]
fn test_frequency_round_trip() {
for f in [
Frequency::Daily,
Frequency::Weekly,
Frequency::Biweekly,
Frequency::Monthly,
Frequency::Yearly,
] {
let s = f.as_str();
let parsed = Frequency::from_str(s).unwrap();
assert_eq!(f, parsed);
}
}
#[test]
fn test_frequency_invalid() {
assert_eq!(Frequency::from_str("quarterly"), None);
}
#[test]
fn test_transaction_type_display() {
assert_eq!(format!("{}", TransactionType::Expense), "expense");
assert_eq!(format!("{}", TransactionType::Income), "income");
}
#[test]
fn test_frequency_display() {
assert_eq!(format!("{}", Frequency::Monthly), "monthly");
assert_eq!(format!("{}", Frequency::Biweekly), "biweekly");
}
}