/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
use std::collections::HashMap;
use std::ops::DerefMut;
use std::{cell::RefCell, future::Future};
use chrono::NaiveDate;
use sqlx::sqlite::SqliteRow;
use sqlx::{Connection, Row, SqliteConnection};
use tokio::runtime::Runtime;
use crate::account_config::AccountConfiguration;
use crate::{util::format_date, QuantityInt};
pub struct DbConnection {
sqlx_connection: RefCell,
}
fn run_blocking(future: F) -> F::Output {
let rt = Runtime::new().unwrap();
rt.block_on(future)
}
impl DbConnection {
/// Connect to the given Sqlite database
pub fn connect(url: &str) -> Self {
Self {
sqlx_connection: RefCell::new(run_blocking(Self::connect_async(url))),
}
}
async fn connect_async(url: &str) -> SqliteConnection {
SqliteConnection::connect(url).await.expect("SQL error")
}
/// Get account balances from the database
pub fn get_balances(&self, date: NaiveDate) -> HashMap {
run_blocking(self.get_balances_async(date))
}
async fn get_balances_async(&self, date: NaiveDate) -> HashMap {
let mut connection = self.sqlx_connection.borrow_mut();
let rows = sqlx::query(
"-- Get last transaction for each account
WITH max_dt_by_account AS (
SELECT account, max(dt) AS max_dt
FROM joined_transactions
WHERE DATE(dt) <= DATE($1)
GROUP BY account
),
max_tid_by_account AS (
SELECT max_dt_by_account.account, max(transaction_id) AS max_tid
FROM max_dt_by_account
JOIN joined_transactions ON max_dt_by_account.account = joined_transactions.account AND max_dt_by_account.max_dt = joined_transactions.dt
GROUP BY max_dt_by_account.account
)
-- Get running balance at last transaction for each account
SELECT max_tid_by_account.account, running_balance AS quantity
FROM max_tid_by_account
JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account"
).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error");
let mut balances = HashMap::new();
for row in rows {
balances.insert(row.get("account"), row.get("quantity"));
}
balances
}
/// Get account configurations from the database
pub fn get_account_configurations(&self) -> Vec {
run_blocking(self.get_account_configurations_async())
}
async fn get_account_configurations_async(&self) -> Vec {
let mut connection = self.sqlx_connection.borrow_mut();
let mut account_configurations =
sqlx::query("SELECT id, account, kind, data FROM account_configurations")
.map(|r: SqliteRow| AccountConfiguration {
id: r.get("id"),
account: r.get("account"),
kind: r.get("kind"),
data: r.get("data"),
})
.fetch_all(connection.deref_mut())
.await
.expect("SQL error");
// System accounts
account_configurations.push(AccountConfiguration {
id: None,
account: "Current Year Earnings".to_string(),
kind: "drcr.equity".to_string(),
data: None,
});
account_configurations.push(AccountConfiguration {
id: None,
account: "Retained Earnings".to_string(),
kind: "drcr.equity".to_string(),
data: None,
});
account_configurations
}
}