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
This commit is contained in:
539
outlay-core/src/seed.rs
Normal file
539
outlay-core/src/seed.rs
Normal file
@@ -0,0 +1,539 @@
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user