- 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
540 lines
22 KiB
Rust
540 lines
22 KiB
Rust
use chrono::{Datelike, Local, NaiveDate};
|
|
use rand::Rng;
|
|
use rusqlite::params;
|
|
|
|
use crate::db::Database;
|
|
|
|
/// Populate the database with realistic demo data spanning ~2 years.
|
|
/// Assumes the database already has default categories seeded.
|
|
pub fn seed_demo_data(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut rng = rand::thread_rng();
|
|
let today = Local::now().date_naive();
|
|
let start = NaiveDate::from_ymd_opt(today.year() - 2, today.month(), 1).unwrap();
|
|
|
|
// -- Settings --
|
|
db.set_setting("base_currency", "USD")?;
|
|
db.set_setting("theme", "system")?;
|
|
|
|
// -- Look up category IDs --
|
|
let cats: Vec<(i64, String, String)> = db.conn.prepare(
|
|
"SELECT id, name, type FROM categories ORDER BY id"
|
|
)?.query_map([], |row| {
|
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
|
|
})?.filter_map(|r| r.ok()).collect();
|
|
|
|
let cat_id = |name: &str| -> i64 {
|
|
cats.iter().find(|(_, n, _)| n == name).map(|(id, _, _)| *id).unwrap_or(1)
|
|
};
|
|
|
|
// Expense category IDs
|
|
let food_id = cat_id("Food and Dining");
|
|
let groceries_id = cat_id("Groceries");
|
|
let transport_id = cat_id("Transport");
|
|
let housing_id = cat_id("Housing/Rent");
|
|
let utilities_id = cat_id("Utilities");
|
|
let entertainment_id = cat_id("Entertainment");
|
|
let shopping_id = cat_id("Shopping");
|
|
let health_id = cat_id("Health");
|
|
let education_id = cat_id("Education");
|
|
let subscriptions_id = cat_id("Subscriptions");
|
|
let personal_id = cat_id("Personal Care");
|
|
let gifts_id = cat_id("Gifts");
|
|
let travel_id = cat_id("Travel");
|
|
|
|
// Income category IDs
|
|
let salary_id = cat_id("Salary");
|
|
let freelance_id = cat_id("Freelance");
|
|
let investment_id = cat_id("Investment");
|
|
let gift_income_id = cat_id("Gift");
|
|
let refund_id = cat_id("Refund");
|
|
|
|
// Realistic payees and notes per category
|
|
let food_payees = ["Chipotle", "Starbucks", "Panda Express", "Subway", "Pizza Hut",
|
|
"Local Diner", "Thai Kitchen", "Burger Joint", "Sushi Bar", "Taco Bell"];
|
|
let grocery_payees = ["Whole Foods", "Trader Joe's", "Kroger", "Costco", "Aldi",
|
|
"Safeway", "Target", "Walmart"];
|
|
let transport_notes = ["Gas station", "Bus pass", "Uber ride", "Lyft", "Parking",
|
|
"Car wash", "Oil change", "Tire rotation"];
|
|
let entertainment_notes = ["Movie tickets", "Netflix", "Concert", "Board game",
|
|
"Bowling", "Escape room", "Museum", "Book"];
|
|
let shopping_payees = ["Amazon", "Target", "Best Buy", "IKEA", "Home Depot",
|
|
"Etsy", "Thrift store"];
|
|
let health_notes = ["Pharmacy", "Doctor copay", "Gym membership", "Vitamins",
|
|
"Dentist", "Eye exam"];
|
|
let personal_notes = ["Haircut", "Toiletries", "Dry cleaning", "Laundry"];
|
|
|
|
// Helper: random float in range
|
|
let rand_amount = |rng: &mut rand::rngs::ThreadRng, low: f64, high: f64| -> f64 {
|
|
let val = rng.gen_range(low..high);
|
|
(val * 100.0).round() / 100.0
|
|
};
|
|
|
|
let rand_pick = |rng: &mut rand::rngs::ThreadRng, items: &[&str]| -> String {
|
|
items[rng.gen_range(0..items.len())].to_string()
|
|
};
|
|
|
|
let insert_txn = |date: NaiveDate, amount: f64, txn_type: &str, cat: i64,
|
|
note: Option<&str>, payee: Option<&str>| -> Result<(), Box<dyn std::error::Error>> {
|
|
let date_str = date.format("%Y-%m-%d").to_string();
|
|
let created = format!("{} 12:00:00", date_str);
|
|
db.conn.execute(
|
|
"INSERT INTO transactions (amount, type, category_id, currency, exchange_rate, note, date, created_at, payee)
|
|
VALUES (?1, ?2, ?3, 'USD', 1.0, ?4, ?5, ?6, ?7)",
|
|
params![amount, txn_type, cat, note, date_str, created, payee],
|
|
)?;
|
|
Ok(())
|
|
};
|
|
|
|
// -- Generate transactions month by month --
|
|
let mut current = start;
|
|
while current <= today {
|
|
let year = current.year();
|
|
let month = current.month();
|
|
let days_in_month = if month == 12 {
|
|
NaiveDate::from_ymd_opt(year + 1, 1, 1)
|
|
} else {
|
|
NaiveDate::from_ymd_opt(year, month + 1, 1)
|
|
}.and_then(|d| d.pred_opt()).map(|d| d.day()).unwrap_or(30);
|
|
|
|
let month_str = format!("{}-{:02}", year, month);
|
|
|
|
// Monthly income: salary on the 1st and 15th (biweekly)
|
|
let base_salary = 2850.0 + (year - start.year()) as f64 * 150.0;
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 1) {
|
|
if d <= today {
|
|
insert_txn(d, base_salary, "income", salary_id, Some("Biweekly paycheck"), Some("Acme Corp"))?;
|
|
}
|
|
}
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 15) {
|
|
if d <= today {
|
|
insert_txn(d, base_salary, "income", salary_id, Some("Biweekly paycheck"), Some("Acme Corp"))?;
|
|
}
|
|
}
|
|
|
|
// Occasional freelance income (30% of months)
|
|
if rng.gen_bool(0.3) {
|
|
let day = rng.gen_range(5..=25).min(days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 200.0, 1200.0);
|
|
insert_txn(d, amt, "income", freelance_id, Some("Web dev project"), Some("Freelance client"))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Investment dividends quarterly (March, June, Sept, Dec)
|
|
if matches!(month, 3 | 6 | 9 | 12) {
|
|
let day = rng.gen_range(10..=20).min(days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 50.0, 180.0);
|
|
insert_txn(d, amt, "income", investment_id, Some("Dividend payment"), Some("Vanguard"))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Occasional refunds
|
|
if rng.gen_bool(0.15) {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 10.0, 80.0);
|
|
insert_txn(d, amt, "income", refund_id, Some("Return item"), Some("Amazon"))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Birthday/holiday gift income (December, month of user)
|
|
if month == 12 {
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 25) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 50.0, 200.0);
|
|
insert_txn(d, amt, "income", gift_income_id, Some("Holiday gift"), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- EXPENSES --
|
|
|
|
// Rent: 1st of every month
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 1) {
|
|
if d <= today {
|
|
insert_txn(d, 1350.00, "expense", housing_id, Some("Monthly rent"), Some("Pinewood Apartments"))?;
|
|
}
|
|
}
|
|
|
|
// Utilities: ~10th of month
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 10.min(days_in_month)) {
|
|
if d <= today {
|
|
let electric = rand_amount(&mut rng, 60.0, 140.0);
|
|
insert_txn(d, electric, "expense", utilities_id, Some("Electric bill"), Some("City Power Co"))?;
|
|
let internet = 65.00;
|
|
insert_txn(d, internet, "expense", utilities_id, Some("Internet"), Some("Comcast"))?;
|
|
}
|
|
}
|
|
|
|
// Phone bill: 5th
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 5.min(days_in_month)) {
|
|
if d <= today {
|
|
insert_txn(d, 45.00, "expense", subscriptions_id, Some("Phone plan"), Some("Mint Mobile"))?;
|
|
}
|
|
}
|
|
|
|
// Streaming subscriptions: 1st
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, 1) {
|
|
if d <= today {
|
|
insert_txn(d, 15.99, "expense", subscriptions_id, Some("Streaming service"), Some("Netflix"))?;
|
|
insert_txn(d, 10.99, "expense", subscriptions_id, Some("Music streaming"), Some("Spotify"))?;
|
|
}
|
|
}
|
|
|
|
// Groceries: 2-4 trips per month
|
|
let grocery_trips = rng.gen_range(2..=4);
|
|
for _ in 0..grocery_trips {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 45.0, 160.0);
|
|
let payee = rand_pick(&mut rng, &grocery_payees);
|
|
insert_txn(d, amt, "expense", groceries_id, Some("Weekly groceries"), Some(&payee))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Food and dining: 4-8 meals out per month
|
|
let meals_out = rng.gen_range(4..=8);
|
|
for _ in 0..meals_out {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 8.0, 55.0);
|
|
let payee = rand_pick(&mut rng, &food_payees);
|
|
insert_txn(d, amt, "expense", food_id, None, Some(&payee))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Transport: 2-5 per month
|
|
let transport_count = rng.gen_range(2..=5);
|
|
for _ in 0..transport_count {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 5.0, 65.0);
|
|
let note = rand_pick(&mut rng, &transport_notes);
|
|
insert_txn(d, amt, "expense", transport_id, Some(¬e), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entertainment: 1-3 per month
|
|
let ent_count = rng.gen_range(1..=3);
|
|
for _ in 0..ent_count {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 10.0, 70.0);
|
|
let note = rand_pick(&mut rng, &entertainment_notes);
|
|
insert_txn(d, amt, "expense", entertainment_id, Some(¬e), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shopping: 1-3 per month
|
|
let shop_count = rng.gen_range(1..=3);
|
|
for _ in 0..shop_count {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 15.0, 120.0);
|
|
let payee = rand_pick(&mut rng, &shopping_payees);
|
|
insert_txn(d, amt, "expense", shopping_id, None, Some(&payee))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Health: 0-2 per month
|
|
let health_count = rng.gen_range(0..=2);
|
|
for _ in 0..health_count {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 15.0, 120.0);
|
|
let note = rand_pick(&mut rng, &health_notes);
|
|
insert_txn(d, amt, "expense", health_id, Some(¬e), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Personal care: 0-2 per month
|
|
let personal_count = rng.gen_range(0..=2);
|
|
for _ in 0..personal_count {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 12.0, 60.0);
|
|
let note = rand_pick(&mut rng, &personal_notes);
|
|
insert_txn(d, amt, "expense", personal_id, Some(¬e), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Education: occasional (20% of months)
|
|
if rng.gen_bool(0.2) {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 15.0, 80.0);
|
|
insert_txn(d, amt, "expense", education_id, Some("Online course"), Some("Udemy"))?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gifts: mainly November/December, occasionally otherwise
|
|
let gift_chance = if matches!(month, 11 | 12) { 0.8 } else { 0.1 };
|
|
if rng.gen_bool(gift_chance) {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 20.0, 150.0);
|
|
insert_txn(d, amt, "expense", gifts_id, Some("Birthday/holiday gift"), None)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Travel: 1-2 trips per year (spread across a few months)
|
|
if rng.gen_bool(0.08) {
|
|
for _ in 0..rng.gen_range(2..=4) {
|
|
let day = rng.gen_range(1..=days_in_month);
|
|
if let Some(d) = NaiveDate::from_ymd_opt(year, month, day) {
|
|
if d <= today {
|
|
let amt = rand_amount(&mut rng, 50.0, 400.0);
|
|
let notes = ["Hotel stay", "Flight", "Restaurant abroad", "Sightseeing"];
|
|
let note = rand_pick(&mut rng, ¬es);
|
|
insert_txn(d, amt, "expense", travel_id, Some(¬e), None)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -- Budgets for this month --
|
|
let budget_items: Vec<(i64, f64)> = vec![
|
|
(groceries_id, 500.0),
|
|
(food_id, 350.0),
|
|
(transport_id, 200.0),
|
|
(entertainment_id, 150.0),
|
|
(shopping_id, 200.0),
|
|
(utilities_id, 250.0),
|
|
(subscriptions_id, 80.0),
|
|
(health_id, 100.0),
|
|
(personal_id, 75.0),
|
|
];
|
|
for (cat, amt) in &budget_items {
|
|
db.conn.execute(
|
|
"INSERT OR IGNORE INTO budgets (category_id, amount, month) VALUES (?1, ?2, ?3)",
|
|
params![cat, amt, month_str],
|
|
)?;
|
|
}
|
|
|
|
// Advance to next month
|
|
current = if month == 12 {
|
|
NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
|
|
} else {
|
|
NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
|
|
};
|
|
}
|
|
|
|
// -- Recurring transactions (plain, non-subscription) --
|
|
let two_years_ago = format!("{}-{:02}-01", today.year() - 2, today.month());
|
|
let recurring_items: Vec<(f64, &str, i64, &str, &str)> = vec![
|
|
(1350.00, "expense", housing_id, "monthly", "Monthly rent"),
|
|
(65.00, "expense", utilities_id, "monthly", "Internet"),
|
|
];
|
|
for (amount, txn_type, cat, freq, note) in &recurring_items {
|
|
db.conn.execute(
|
|
"INSERT INTO recurring_transactions (amount, type, category_id, currency, note, frequency, start_date, active)
|
|
VALUES (?1, ?2, ?3, 'USD', ?4, ?5, ?6, 1)",
|
|
params![amount, txn_type, cat, note, freq, two_years_ago],
|
|
)?;
|
|
}
|
|
|
|
// -- Linked subscriptions + recurring --
|
|
use crate::models::{Frequency, NewRecurringTransaction, TransactionType};
|
|
|
|
let sub_services: Vec<(&str, f64, &str, &str)> = vec![
|
|
("Netflix", 15.99, "tabler-brand-netflix", "#E50914"),
|
|
("Spotify", 10.99, "tabler-brand-spotify", "#1DB954"),
|
|
("iCloud", 2.99, "tabler-cloud", "#3693F3"),
|
|
("GitHub", 4.00, "tabler-brand-github", "#333333"),
|
|
("Xbox Game Pass", 16.99, "tabler-brand-xbox", "#107C10"),
|
|
];
|
|
|
|
for (name, amount, _icon, _color) in &sub_services {
|
|
// Find the subscription category by name
|
|
let sub_cat_id: i64 = db.conn.query_row(
|
|
"SELECT id FROM subscription_categories WHERE name = ?1",
|
|
params![name],
|
|
|row| row.get(0),
|
|
).unwrap_or_else(|_| {
|
|
// Fallback to "Other" category
|
|
db.conn.query_row(
|
|
"SELECT id FROM subscription_categories WHERE name = 'Other'",
|
|
[],
|
|
|row| row.get(0),
|
|
).unwrap_or(1)
|
|
});
|
|
|
|
let start = chrono::NaiveDate::parse_from_str(&two_years_ago, "%Y-%m-%d")
|
|
.unwrap_or(today);
|
|
|
|
let new_rec = NewRecurringTransaction {
|
|
amount: *amount,
|
|
transaction_type: TransactionType::Expense,
|
|
category_id: subscriptions_id,
|
|
currency: "USD".to_string(),
|
|
note: Some(name.to_string()),
|
|
frequency: Frequency::Monthly,
|
|
start_date: start,
|
|
end_date: None,
|
|
is_bill: true,
|
|
reminder_days: 3,
|
|
subscription_id: None,
|
|
};
|
|
db.insert_linked_recurring_and_subscription(&new_rec, sub_cat_id, name)?;
|
|
}
|
|
|
|
// -- Savings goals --
|
|
db.conn.execute(
|
|
"INSERT INTO savings_goals (name, target, saved, currency, deadline, color, icon)
|
|
VALUES ('Emergency Fund', 10000.0, 6450.0, 'USD', ?1, '#27ae60', 'tabler-shield')",
|
|
params![format!("{}-12-31", today.year())],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO savings_goals (name, target, saved, currency, deadline, color, icon)
|
|
VALUES ('Vacation Fund', 3000.0, 1820.0, 'USD', ?1, '#3498db', 'tabler-plane')",
|
|
params![format!("{}-06-30", today.year() + 1)],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO savings_goals (name, target, saved, currency, deadline, color, icon)
|
|
VALUES ('New Laptop', 1500.0, 950.0, 'USD', ?1, '#9b59b6', 'tabler-device-laptop')",
|
|
params![format!("{}-09-01", today.year())],
|
|
)?;
|
|
|
|
// -- Wishlist items --
|
|
db.conn.execute(
|
|
"INSERT INTO wishlist_items (name, amount, category_id, note, priority)
|
|
VALUES ('Noise Cancelling Headphones', 299.99, ?1, 'Sony WH-1000XM5', 1)",
|
|
params![shopping_id],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO wishlist_items (name, amount, category_id, note, priority)
|
|
VALUES ('Ergonomic Keyboard', 179.00, ?1, 'Kinesis Advantage 360', 2)",
|
|
params![shopping_id],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO wishlist_items (name, amount, category_id, note, priority)
|
|
VALUES ('Camping Gear Set', 450.00, ?1, 'Tent + sleeping bag + mat', 3)",
|
|
params![travel_id],
|
|
)?;
|
|
|
|
// -- Credit Cards --
|
|
db.conn.execute(
|
|
"INSERT INTO credit_cards (name, credit_limit, statement_close_day, due_day, min_payment_pct, current_balance, currency, color)
|
|
VALUES ('Chase Sapphire', 8000.0, 25, 15, 2.0, 2340.0, 'USD', '#003087')",
|
|
[],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO credit_cards (name, credit_limit, statement_close_day, due_day, min_payment_pct, current_balance, currency, color)
|
|
VALUES ('Amex Gold', 12000.0, 20, 10, 2.0, 890.0, 'USD', '#C4A000')",
|
|
[],
|
|
)?;
|
|
|
|
// -- Purchased wishlist items --
|
|
db.conn.execute(
|
|
"INSERT INTO wishlist_items (name, amount, category_id, note, priority, purchased)
|
|
VALUES ('Mechanical Keyboard', 149.99, ?1, 'Cherry MX Brown switches', 2, 1)",
|
|
params![shopping_id],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO wishlist_items (name, amount, category_id, note, priority, purchased)
|
|
VALUES ('Running Shoes', 89.99, ?1, 'Nike Pegasus', 1, 1)",
|
|
params![shopping_id],
|
|
)?;
|
|
|
|
// -- Achievements --
|
|
let two_years_ago_dt = format!(
|
|
"{}-{:02}-15 12:00:00",
|
|
today.year() - 2,
|
|
today.month()
|
|
);
|
|
let one_year_ago_dt = format!(
|
|
"{}-{:02}-15 12:00:00",
|
|
today.year() - 1,
|
|
today.month()
|
|
);
|
|
let six_months_ago_dt = {
|
|
let m = if today.month() > 6 { today.month() - 6 } else { today.month() + 6 };
|
|
let y = if today.month() > 6 { today.year() } else { today.year() - 1 };
|
|
format!("{}-{:02}-15 12:00:00", y, m)
|
|
};
|
|
db.conn.execute(
|
|
"UPDATE achievements SET earned_at = ?1 WHERE name = 'First Transaction'",
|
|
params![two_years_ago_dt],
|
|
)?;
|
|
db.conn.execute(
|
|
"UPDATE achievements SET earned_at = ?1 WHERE name = '100 Transactions'",
|
|
params![one_year_ago_dt],
|
|
)?;
|
|
db.conn.execute(
|
|
"UPDATE achievements SET earned_at = ?1 WHERE name = 'Month Under Budget'",
|
|
params![six_months_ago_dt],
|
|
)?;
|
|
|
|
// -- Transaction Templates --
|
|
db.insert_template(
|
|
"Morning Coffee",
|
|
Some(5.50),
|
|
TransactionType::Expense,
|
|
food_id,
|
|
"USD",
|
|
Some("Starbucks"),
|
|
Some("Daily coffee"),
|
|
None,
|
|
)?;
|
|
db.insert_template(
|
|
"Weekly Groceries",
|
|
Some(85.00),
|
|
TransactionType::Expense,
|
|
groceries_id,
|
|
"USD",
|
|
Some("Trader Joe's"),
|
|
Some("Weekly grocery run"),
|
|
None,
|
|
)?;
|
|
|
|
// -- Tags --
|
|
db.conn.execute("INSERT OR IGNORE INTO tags (name) VALUES ('essential')", [])?;
|
|
db.conn.execute("INSERT OR IGNORE INTO tags (name) VALUES ('splurge')", [])?;
|
|
db.conn.execute("INSERT OR IGNORE INTO tags (name) VALUES ('recurring')", [])?;
|
|
db.conn.execute("INSERT OR IGNORE INTO tags (name) VALUES ('work-related')", [])?;
|
|
|
|
// -- Categorization rules --
|
|
db.conn.execute(
|
|
"INSERT INTO categorization_rules (field, pattern, category_id, priority)
|
|
VALUES ('payee', 'Starbucks', ?1, 1)",
|
|
params![food_id],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO categorization_rules (field, pattern, category_id, priority)
|
|
VALUES ('payee', 'Whole Foods', ?1, 1)",
|
|
params![groceries_id],
|
|
)?;
|
|
db.conn.execute(
|
|
"INSERT INTO categorization_rules (field, pattern, category_id, priority)
|
|
VALUES ('payee', 'Amazon', ?1, 1)",
|
|
params![shopping_id],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|