summaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-10-30 07:27:13 +0000
committerJoe Carstairs <me@joeac.net>2024-10-30 08:50:45 +0000
commit828b9ae9f6217f7e5e97ec704134c786dd401bad (patch)
treec920b8874682e89ba05b6e1ca20275bfd5878d87 /backend
parent6e9f0069f5d1e4cfbf7f9feb8536795a78c818f6 (diff)
Separates date_utc and datetime_utc
Diffstat (limited to 'backend')
-rw-r--r--backend/core/migrations/2024-08-31-084439_initial_setup/up.sql1
-rw-r--r--backend/core/src/lib.rs1
-rw-r--r--backend/core/src/models/account.rs4
-rw-r--r--backend/core/src/models/budget.rs6
-rw-r--r--backend/core/src/models/budget_update.rs8
-rw-r--r--backend/core/src/models/date_time_utc.rs182
-rw-r--r--backend/core/src/models/date_utc.rs309
-rw-r--r--backend/core/src/models/datetime_utc.rs223
-rw-r--r--backend/core/src/models/mod.rs3
-rw-r--r--backend/core/src/models/transaction.rs4
-rw-r--r--backend/core/src/queries/budget_updates/get_first_with_date.rs13
-rw-r--r--backend/core/src/queries/budget_updates/get_most_recent_with_category_id.rs8
-rw-r--r--backend/core/src/queries/categories/get_all_balances.rs2
-rw-r--r--backend/core/src/queries/categories/get_most_recent_budget_per_category_id.rs6
-rw-r--r--backend/core/src/traits/dateable.rs5
-rw-r--r--backend/core/src/traits/mod.rs3
-rw-r--r--backend/core/src/traits/nowlike.rs3
-rw-r--r--backend/core/src/traits/timeable.rs5
-rw-r--r--backend/core/src/utils/calculate_budgets_accrual.rs67
-rw-r--r--backend/core/src/utils/calculate_category_balance.rs35
-rw-r--r--backend/core/tests/common/fakes/account.rs4
-rw-r--r--backend/core/tests/common/fakes/budget_update.rs4
-rw-r--r--backend/core/tests/common/fakes/transaction.rs4
-rw-r--r--backend/core/tests/models/date_time_utc.rs59
-rw-r--r--backend/core/tests/models/date_utc.rs60
-rw-r--r--backend/core/tests/models/datetime_utc.rs60
-rw-r--r--backend/core/tests/models/mod.rs3
-rw-r--r--backend/core/tests/queries/budget_updates.rs212
-rw-r--r--backend/core/tests/queries/categories.rs15
-rw-r--r--backend/core/tests/queries/category_transfers.rs10
-rw-r--r--backend/core/tests/queries/transaction_categorisations.rs6
31 files changed, 985 insertions, 340 deletions
diff --git a/backend/core/migrations/2024-08-31-084439_initial_setup/up.sql b/backend/core/migrations/2024-08-31-084439_initial_setup/up.sql
index 3584ae7..9038b54 100644
--- a/backend/core/migrations/2024-08-31-084439_initial_setup/up.sql
+++ b/backend/core/migrations/2024-08-31-084439_initial_setup/up.sql
@@ -13,6 +13,7 @@ CREATE TABLE budget_updates(
date TEXT NOT NULL,
new_budget INTEGER NOT NULL,
new_period INTEGER NOT NULL,
+ UNIQUE (category_id, date),
FOREIGN KEY (category_id)
REFERENCES categories (id)
ON UPDATE CASCADE
diff --git a/backend/core/src/lib.rs b/backend/core/src/lib.rs
index cb65fe1..fd56999 100644
--- a/backend/core/src/lib.rs
+++ b/backend/core/src/lib.rs
@@ -3,6 +3,7 @@ use diesel_migrations::{embed_migrations, EmbeddedMigrations};
pub mod models;
pub mod queries;
pub mod schema;
+pub mod traits;
mod utils;
diff --git a/backend/core/src/models/account.rs b/backend/core/src/models/account.rs
index fcd5270..2df43e1 100644
--- a/backend/core/src/models/account.rs
+++ b/backend/core/src/models/account.rs
@@ -1,7 +1,7 @@
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
-use crate::models::date_time_utc::DateTimeUtc;
+use crate::models::datetime_utc::DatetimeUtc;
#[derive(
Insertable, Queryable, Identifiable, Selectable, Debug, PartialEq, Clone, Serialize, Deserialize,
@@ -11,5 +11,5 @@ pub struct Account {
pub id: i32,
pub name: String,
pub opening_balance: i32,
- pub opening_date: DateTimeUtc,
+ pub opening_date: DatetimeUtc,
}
diff --git a/backend/core/src/models/budget.rs b/backend/core/src/models/budget.rs
index 08a1c72..45ab534 100644
--- a/backend/core/src/models/budget.rs
+++ b/backend/core/src/models/budget.rs
@@ -19,9 +19,7 @@ impl From<&BudgetUpdate> for Budget {
#[cfg(test)]
mod test {
- use chrono::Utc;
-
- use crate::models::date_time_utc::DateTimeUtc;
+ use crate::{models::date_utc::DateUtc, traits::nowlike::Nowlike};
use super::*;
@@ -32,7 +30,7 @@ mod test {
let budget_update = BudgetUpdate {
id: 0,
category_id: 1,
- date: DateTimeUtc::new(&Utc::now()),
+ date: DateUtc::now(),
new_budget: quantity,
new_period: period,
};
diff --git a/backend/core/src/models/budget_update.rs b/backend/core/src/models/budget_update.rs
index 3500d8e..c7221b1 100644
--- a/backend/core/src/models/budget_update.rs
+++ b/backend/core/src/models/budget_update.rs
@@ -2,17 +2,15 @@ use derive_builder::Builder;
use diesel::prelude::*;
use serde::Serialize;
-use crate::models::category::Category;
+use crate::models::{category::Category, date_utc::DateUtc};
-use super::date_time_utc::DateTimeUtc;
-
-#[derive(Builder, Clone, Serialize, Queryable, Identifiable, Selectable, Associations, Debug, PartialEq, Insertable)]
+#[derive(Builder, Clone, Serialize, Queryable, Identifiable, Selectable, Associations, Debug, PartialEq, Eq, Insertable)]
#[diesel(table_name = crate::schema::budget_updates)]
#[diesel(belongs_to(Category))]
pub struct BudgetUpdate {
pub id: i32,
pub category_id: i32,
- pub date: DateTimeUtc,
+ pub date: DateUtc,
pub new_budget: i32,
pub new_period: i32,
}
diff --git a/backend/core/src/models/date_time_utc.rs b/backend/core/src/models/date_time_utc.rs
deleted file mode 100644
index d59d8f4..0000000
--- a/backend/core/src/models/date_time_utc.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use anyhow::{Error, Result};
-use chrono::{DateTime, Datelike, Utc};
-use diesel::{
- backend::Backend,
- deserialize::{self, FromSql, FromSqlRow},
- expression::AsExpression,
- serialize::{self, ToSql},
- sql_types::Text,
- sqlite::Sqlite,
-};
-use serde::{Deserialize, Serialize};
-
-#[derive(
- Eq,
- PartialEq,
- Ord,
- PartialOrd,
- Serialize,
- Deserialize,
- Clone,
- Debug,
- AsExpression,
- FromSqlRow,
-)]
-#[diesel(sql_type=Text)]
-pub struct DateTimeUtc {
- chrono_datetime_utc: DateTime<Utc>,
-}
-
-impl DateTimeUtc {
- pub fn day_diff(&self, other: &DateTimeUtc) -> u32 {
- self.chrono_datetime_utc
- .num_days_from_ce()
- .abs_diff(other.chrono_datetime_utc.num_days_from_ce())
- }
-
- pub fn new(chrono_datetime_utc: &DateTime<Utc>) -> Self {
- Self {
- chrono_datetime_utc: chrono_datetime_utc.clone(),
- }
- }
-
- pub fn now() -> Self {
- Self::new(&Utc::now())
- }
-
- /// Constructs DateTimeUtc from a day string, YYYY-MM-DD, in UTC
- /// Example: "2024-10-24" -> Midnight 24 Oct 2024 UTC
- pub fn from_day_str(day_str: &str) -> Result<Self> {
- if !Self::is_day_str(day_str) {
- Err(Error::msg(format!("{} is not a day string in YYYY-MM-DD format", day_str)))
- } else {
- let mut date_str = String::from(day_str);
- date_str.push_str("T00:00:00.000Z");
- Ok(Self::from_date_str(&date_str)?)
- }
- }
-
- /// Constructs DateTimeUtc from a date string in ISO format, in UTC
- /// Example: "2024-10-24T10:30:00.000" -> 10.30am 24 Oct 2024 UTC
- pub fn from_date_str(date_time_utc: &str) -> Result<Self> {
- let chrono_datetime_utc = date_time_utc.parse::<DateTime<Utc>>()?;
- Ok(Self {
- chrono_datetime_utc,
- })
- }
-
- /// Converts to a day string, YYYY-MM-DD, in UTC
- /// Example: 10.30am 24 Oct 2024 UTC -> "2024-10-24"
- pub fn to_day_str(&self) -> String {
- let date_str = self.to_date_str();
- let day_str = date_str
- .split_at_checked(10)
- .unwrap()
- .0;
- String::from(day_str)
- }
-
- /// Converts to a date string in ISO format, in UTC
- /// Example: 10.30am 24 Oct 2024 UTC -> "2024-10-24T10:30:00.000Z"
- pub fn to_date_str(&self) -> String {
- self.chrono_datetime_utc.to_rfc3339()
- }
-
-
- fn is_day_str(str: &str) -> bool {
- if str.len() != 10 {
- return false;
- }
- for (ix, ch) in str.chars().enumerate() {
- if ix == 4 || ix == 7 {
- if ch != '-' {
- return false;
- }
- } else {
- if ch < '0' || ch > '9' {
- return false;
- }
- }
- }
- return true;
- }
-}
-
-impl ToSql<Text, Sqlite> for DateTimeUtc {
- fn to_sql<'b>(
- &'b self,
- out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
- ) -> serialize::Result {
- out.set_value(self.to_date_str());
- Ok(serialize::IsNull::No)
- }
-}
-
-impl FromSql<Text, Sqlite> for DateTimeUtc {
- fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<DateTimeUtc> {
- let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
- Ok(Self::from_date_str(&str)?)
- }
-}
-
-#[cfg(test)]
-mod test {
- use chrono::TimeZone;
-
- use super::*;
-
- #[test]
- fn given_midnight_when_day_diff_next_midnight_less_1s_then_return_0() {
- let start = DateTimeUtc::new(&Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().to_utc());
- let end = DateTimeUtc::new(
- &Utc.with_ymd_and_hms(2000, 1, 1, 23, 59, 59)
- .unwrap()
- .to_utc(),
- );
-
- let day_diff = start.day_diff(&end);
- let neg_day_diff = end.day_diff(&start);
-
- assert_eq!(day_diff, 0);
- assert_eq!(day_diff, neg_day_diff);
- }
-
- #[test]
- fn given_midnight_when_day_diff_next_midnight_then_return_1() {
- let start = DateTimeUtc::new(&Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().to_utc());
- let end = DateTimeUtc::new(&Utc.with_ymd_and_hms(2000, 1, 2, 0, 0, 0).unwrap().to_utc());
-
- let day_diff = start.day_diff(&end);
- let neg_day_diff = end.day_diff(&start);
-
- assert_eq!(day_diff, 1);
- assert_eq!(day_diff, neg_day_diff);
- }
-
- #[test]
- fn given_midnight_less_1s_when_day_diff_midnight_then_return_1() {
- let start = DateTimeUtc::new(
- &Utc.with_ymd_and_hms(2000, 1, 1, 23, 59, 59)
- .unwrap()
- .to_utc(),
- );
- let end = DateTimeUtc::new(&Utc.with_ymd_and_hms(2000, 1, 2, 0, 0, 0).unwrap().to_utc());
-
- let day_diff = start.day_diff(&end);
- let neg_day_diff = end.day_diff(&start);
-
- assert_eq!(day_diff, 1);
- assert_eq!(day_diff, neg_day_diff);
- }
-
- #[test]
- fn given_23_nov_1970_when_to_day_str_then_return_iso_day_str() {
- let date_time_utc = DateTimeUtc::new(
- &Utc.with_ymd_and_hms(1970, 11, 23, 0, 0, 0)
- .unwrap()
- .to_utc(),
- );
-
- assert_eq!(date_time_utc.to_day_str(), "1970-11-23");
- }
-}
diff --git a/backend/core/src/models/date_utc.rs b/backend/core/src/models/date_utc.rs
new file mode 100644
index 0000000..55d9e3f
--- /dev/null
+++ b/backend/core/src/models/date_utc.rs
@@ -0,0 +1,309 @@
+use std::{borrow::Borrow, fmt, str::FromStr};
+
+use anyhow::{Context, Error, Result};
+use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Utc};
+use diesel::{
+ backend::Backend,
+ deserialize::{self, FromSql, FromSqlRow},
+ expression::AsExpression,
+ serialize::{self, ToSql},
+ sql_types::Text,
+ sqlite::Sqlite,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::traits::{dateable::Dateable, nowlike::Nowlike};
+
+use super::datetime_utc::DatetimeUtc;
+
+#[derive(
+ Eq,
+ PartialEq,
+ Ord,
+ PartialOrd,
+ Serialize,
+ Deserialize,
+ Clone,
+ Debug,
+ AsExpression,
+ FromSqlRow,
+)]
+#[diesel(sql_type=Text)]
+pub struct DateUtc {
+ chrono_datetime_utc: DateTime<Utc>,
+}
+
+impl DateUtc {
+ pub fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self> {
+ Ok(Self {
+ chrono_datetime_utc: NaiveDate::from_ymd_opt(year, month, day)
+ .with_context(|| format!("failed to construct date from ymd {:04}-{:02}-{:02}", year, month, day))?
+ .and_hms_opt(0, 0, 0).unwrap()
+ .and_utc()
+ })
+ }
+
+ pub fn with_hms(&mut self, hour: u32, min: u32, sec: u32) -> Result<DatetimeUtc> {
+ DatetimeUtc::from_ymd_and_hms(
+ self.year(), self.month(), self.day(), hour, min, sec,
+ )
+ }
+
+ pub fn day_diff(&self, other: &DateUtc) -> u32 {
+ self.chrono_datetime_utc
+ .num_days_from_ce()
+ .abs_diff(other.chrono_datetime_utc.num_days_from_ce())
+ }
+
+ fn is_day_str(str: &str) -> bool {
+ if str.len() != 10 {
+ return false;
+ }
+ for (ix, ch) in str.chars().enumerate() {
+ if ix == 4 || ix == 7 {
+ if ch != '-' {
+ return false;
+ }
+ } else {
+ if ch < '0' || ch > '9' {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
+
+impl Nowlike for DateUtc {
+ fn now() -> Self {
+ let now = Utc::now();
+ Self::from_ymd(
+ now.year(),
+ now.month(),
+ now.day(),
+ ).unwrap()
+ }
+}
+
+impl Dateable for DateUtc {
+ fn year(&self) -> i32 {
+ self.chrono_datetime_utc.year()
+ }
+
+ fn month(&self) -> u32 {
+ self.chrono_datetime_utc.month()
+ }
+
+ fn day(&self) -> u32 {
+ self.chrono_datetime_utc.day()
+ }
+}
+
+impl From<DatetimeUtc> for DateUtc {
+ fn from(datetime_utc: DatetimeUtc) -> Self {
+ Self {
+ chrono_datetime_utc: datetime_utc
+ .borrow()
+ .to_string()
+ .parse::<DateTime<Utc>>().unwrap()
+ .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()).unwrap(),
+ }
+ }
+}
+
+impl<T> From<T> for DateUtc where T: Borrow<DateTime<Utc>> {
+ fn from(chrono_datetime_utc: T) -> Self {
+ Self {
+ chrono_datetime_utc: chrono_datetime_utc
+ .borrow()
+ .with_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()).unwrap(),
+ }
+ }
+}
+
+impl fmt::Display for DateUtc {
+ /// Converts to a day string, YYYY-MM-DD, in UTC
+ /// Example: 10.30am 24 Oct 2024 UTC -> "2024-10-24"
+ fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ let date_str = self.chrono_datetime_utc.to_rfc3339();
+ let day_str = date_str
+ .split_at_checked(10)
+ .unwrap()
+ .0;
+ write!(f, "{}", day_str)
+ }
+}
+
+impl FromStr for DateUtc {
+ type Err = anyhow::Error;
+
+ /// Constructs DatetimeUtc from a day string, YYYY-MM-DD, in UTC
+ /// Example: "2024-10-24" -> Midnight 24 Oct 2024 UTC
+ fn from_str(date_str: &str) -> Result<Self> {
+ if !Self::is_day_str(&date_str) {
+ Err(Error::msg(format!("{} is not a day string in YYYY-MM-DD format", date_str)))
+ } else {
+ let mut datetime_str = String::from(date_str);
+ datetime_str.push_str("T00:00:00.000Z");
+ Ok(Self {
+ chrono_datetime_utc: datetime_str.parse()?,
+ })
+ }
+ }
+}
+
+impl ToSql<Text, Sqlite> for DateUtc {
+ fn to_sql<'b>(
+ &'b self,
+ out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
+ ) -> serialize::Result {
+ out.set_value(self.to_string());
+ Ok(serialize::IsNull::No)
+ }
+}
+
+impl FromSql<Text, Sqlite> for DateUtc {
+ fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<DateUtc> {
+ let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
+ Ok(str.parse()?)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::traits::timeable::Timeable;
+
+ use super::*;
+
+ #[test]
+ fn when_from_ymd_then_ymd_are_as_expected() {
+ let date_utc = DateUtc::from_ymd(1970, 11, 23);
+
+ assert!(date_utc.is_ok());
+ let date_utc = date_utc.unwrap();
+ assert_eq!(date_utc.year(), 1970);
+ assert_eq!(date_utc.month(), 11);
+ assert_eq!(date_utc.day(), 23);
+ }
+
+ #[test]
+ fn when_from_ymd_then_to_string_as_expected() {
+ let date_utc = DateUtc::from_ymd(1970, 11, 23);
+
+ assert!(date_utc.is_ok());
+ assert_eq!(date_utc.unwrap().to_string(), "1970-11-23");
+ }
+
+ #[test]
+ fn when_from_invalid_ymd_then_err() {
+ let date_utc_1 = DateUtc::from_ymd(1970, 0, 23);
+ let date_utc_2 = DateUtc::from_ymd(1970, 13, 23);
+ let date_utc_3 = DateUtc::from_ymd(1970, 11, 0);
+ let date_utc_4 = DateUtc::from_ymd(1970, 11, 31);
+ let date_utc_5 = DateUtc::from_ymd(1971, 2, 29);
+
+ assert!(date_utc_1.is_err());
+ assert_eq!(date_utc_1.unwrap_err().to_string(), "failed to construct date from ymd 1970-00-23");
+ assert!(date_utc_2.is_err());
+ assert_eq!(date_utc_2.unwrap_err().to_string(), "failed to construct date from ymd 1970-13-23");
+ assert!(date_utc_3.is_err());
+ assert_eq!(date_utc_3.unwrap_err().to_string(), "failed to construct date from ymd 1970-11-00");
+ assert!(date_utc_4.is_err());
+ assert_eq!(date_utc_4.unwrap_err().to_string(), "failed to construct date from ymd 1970-11-31");
+ assert!(date_utc_5.is_err());
+ assert_eq!(date_utc_5.unwrap_err().to_string(), "failed to construct date from ymd 1971-02-29");
+ }
+
+ #[test]
+ fn when_from_string_then_ymd_are_as_expected() {
+ let date_utc: Result<DateUtc> = "1970-11-23".parse();
+
+ assert!(date_utc.is_ok());
+ let date_utc = date_utc.unwrap();
+ assert_eq!(date_utc.year(), 1970);
+ assert_eq!(date_utc.month(), 11);
+ assert_eq!(date_utc.day(), 23);
+ }
+
+ #[test]
+ fn when_from_string_then_to_string_as_expected() {
+ let date_utc: Result<DateUtc> = "1970-11-23".parse();
+
+ assert!(date_utc.is_ok());
+ assert_eq!(date_utc.unwrap().to_string(), "1970-11-23");
+ }
+
+ #[test]
+ fn when_from_invalid_string_then_err() {
+ let date_utc_1: Result<DateUtc> = "1970-00-23".parse();
+ let date_utc_2: Result<DateUtc> = "1970-13-23".parse();
+ let date_utc_3: Result<DateUtc> = "1970-11-0".parse();
+ let date_utc_4: Result<DateUtc> = "1970-11-31".parse();
+ let date_utc_5: Result<DateUtc> = "1971-02-29".parse();
+
+ assert!(date_utc_1.is_err());
+ assert!(date_utc_2.is_err());
+ assert!(date_utc_3.is_err());
+ assert!(date_utc_4.is_err());
+ assert!(date_utc_5.is_err());
+ }
+
+ #[test]
+ fn with_with_hms_then_ymd_hms_are_as_expected() {
+ let datetime_utc = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(12, 55, 16);
+
+ assert!(datetime_utc.is_ok());
+ let datetime_utc = datetime_utc.unwrap();
+ assert_eq!(datetime_utc.year(), 1970);
+ assert_eq!(datetime_utc.month(), 11);
+ assert_eq!(datetime_utc.day(), 23);
+ assert_eq!(datetime_utc.hour(), 12);
+ assert_eq!(datetime_utc.minute(), 55);
+ assert_eq!(datetime_utc.second(), 16);
+ }
+
+ #[test]
+ fn when_with_invalid_hms_then_err() {
+ let datetime_utc_1 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(25, 0, 0);
+ let datetime_utc_2 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(0, 61, 0);
+ let datetime_utc_3 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(0, 0, 61);
+
+ assert!(datetime_utc_1.is_err());
+ assert!(datetime_utc_2.is_err());
+ assert!(datetime_utc_3.is_err());
+ }
+
+ #[test]
+ fn when_from_datetime_utc_at_different_times_on_same_day_then_eq() {
+ let datetime_utc1 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(0, 0, 0).unwrap();
+ let datetime_utc2 = DateUtc::from_ymd(1970, 11, 23).unwrap().with_hms(23, 59, 59).unwrap();
+ let date_utc1 = DateUtc::from(datetime_utc1);
+ let date_utc2 = DateUtc::from(datetime_utc2);
+
+ assert_eq!(date_utc1, date_utc2);
+ }
+
+ #[test]
+ fn given_today_when_day_diff_today_then_return_0() {
+ let start = DateUtc::from_ymd(2000, 1, 1).unwrap();
+ let end = DateUtc::from_ymd(2000, 1, 1).unwrap();
+
+ let day_diff = start.day_diff(&end);
+ let neg_day_diff = end.day_diff(&start);
+
+ assert_eq!(day_diff, 0);
+ assert_eq!(day_diff, neg_day_diff);
+ }
+
+ #[test]
+ fn given_today_when_day_diff_tomorrow_then_return_1() {
+ let start = DateUtc::from_ymd(2000, 1, 1).unwrap();
+ let end = DateUtc::from_ymd(2000, 1, 2).unwrap();
+
+ let day_diff = start.day_diff(&end);
+ let neg_day_diff = end.day_diff(&start);
+
+ assert_eq!(day_diff, 1);
+ assert_eq!(day_diff, neg_day_diff);
+ }
+}
diff --git a/backend/core/src/models/datetime_utc.rs b/backend/core/src/models/datetime_utc.rs
new file mode 100644
index 0000000..98d1d6c
--- /dev/null
+++ b/backend/core/src/models/datetime_utc.rs
@@ -0,0 +1,223 @@
+use std::{fmt::Display, str::FromStr};
+
+use anyhow::{Context, Result};
+use chrono::{DateTime, Datelike as ChronoDatelike, NaiveDate, Timelike as ChronoTimelike, Utc};
+use diesel::{
+ backend::Backend,
+ deserialize::{self, FromSql, FromSqlRow},
+ expression::AsExpression,
+ serialize::{self, ToSql},
+ sql_types::Text,
+ sqlite::Sqlite,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::traits::{dateable::Dateable, nowlike::Nowlike, timeable::Timeable};
+
+#[derive(
+ PartialEq,
+ Eq,
+ PartialOrd,
+ Ord,
+ Serialize,
+ Deserialize,
+ Clone,
+ Debug,
+ AsExpression,
+ FromSqlRow,
+)]
+#[diesel(sql_type=Text)]
+pub struct DatetimeUtc {
+ chrono_datetime_utc: DateTime<Utc>,
+}
+
+impl DatetimeUtc {
+ pub fn from_ymd_and_hms(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Result<Self> {
+ Ok(Self { chrono_datetime_utc: NaiveDate
+ ::from_ymd_opt(year, month, day)
+ .with_context(|| format!("failed to construct datetime from ymd {:04}-{:02}-{:02}", year, month, day))?
+ .and_hms_opt(hour, min, sec)
+ .with_context(|| format!("failed to construct datetime from hms {:02}:{:02}:{:02}", hour, min, sec))?
+ .and_utc()
+ })
+ }
+}
+
+impl FromStr for DatetimeUtc {
+ type Err = anyhow::Error;
+
+ /// Constructs DatetimeUtc from a date string in ISO format, in UTC
+ /// Example: "2024-10-24T10:30:00.000" -> 10.30am 24 Oct 2024 UTC
+ fn from_str(datetime_str: &str) -> Result<Self> {
+ let chrono_datetime_utc = datetime_str.parse::<DateTime<Utc>>()?;
+ Ok(Self {
+ chrono_datetime_utc,
+ })
+ }
+}
+
+impl Display for DatetimeUtc {
+ /// Converts to a date string in ISO format, in UTC
+ /// Example: 10.30am 24 Oct 2024 UTC -> "2024-10-24T10:30:00.000Z"
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.chrono_datetime_utc.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
+ }
+}
+
+impl Nowlike for DatetimeUtc {
+ fn now() -> Self {
+ let now = Utc::now();
+ Self::from_ymd_and_hms(
+ now.year(),
+ now.month(),
+ now.day(),
+ now.hour(),
+ now.minute(),
+ now.second(),
+ ).unwrap()
+ }
+}
+
+impl Dateable for DatetimeUtc {
+ fn year(&self) -> i32 {
+ self.chrono_datetime_utc.year()
+ }
+
+ fn month(&self) -> u32 {
+ self.chrono_datetime_utc.month()
+ }
+
+ fn day(&self) -> u32 {
+ self.chrono_datetime_utc.day()
+ }
+}
+
+impl Timeable for DatetimeUtc {
+ fn hour(&self) -> u32 {
+ self.chrono_datetime_utc.hour()
+ }
+
+ fn minute(&self) -> u32 {
+ self.chrono_datetime_utc.minute()
+ }
+
+ fn second(&self) -> u32 {
+ self.chrono_datetime_utc.second()
+ }
+}
+
+impl ToSql<Text, Sqlite> for DatetimeUtc {
+ fn to_sql<'b>(
+ &'b self,
+ out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
+ ) -> serialize::Result {
+ out.set_value::<String>(self.to_string());
+ Ok(serialize::IsNull::No)
+ }
+}
+
+impl FromSql<Text, Sqlite> for DatetimeUtc {
+ fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<DatetimeUtc> {
+ let str = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
+ Ok(str.parse()?)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn when_from_ymd_and_hms_then_ymd_and_hms_are_as_expected() {
+ let datetime_utc = DatetimeUtc::from_ymd_and_hms(1970, 11, 23, 12, 55, 16);
+
+ assert!(datetime_utc.is_ok());
+ let date_utc = datetime_utc.unwrap();
+ assert_eq!(date_utc.year(), 1970);
+ assert_eq!(date_utc.month(), 11);
+ assert_eq!(date_utc.day(), 23);
+ assert_eq!(date_utc.hour(), 12);
+ assert_eq!(date_utc.minute(), 55);
+ assert_eq!(date_utc.second(), 16);
+ }
+
+ #[test]
+ fn when_from_ymd_and_hms_then_to_string_as_expected() {
+ let date_utc = DatetimeUtc::from_ymd_and_hms(1970, 11, 23, 12, 55, 16);
+
+ assert!(date_utc.is_ok());
+ assert_eq!(date_utc.unwrap().to_string(), "1970-11-23T12:55:16.000Z")
+ }
+
+ #[test]
+ fn when_with_invalid_ymd_and_hms_then_err() {
+ let datetime_utc_1 = DatetimeUtc::from_ymd_and_hms(1970, 0, 23, 12, 55, 16);
+ let datetime_utc_2 = DatetimeUtc::from_ymd_and_hms(1970, 13, 23, 12, 55, 16);
+ let datetime_utc_3 = DatetimeUtc::from_ymd_and_hms(1970, 11, 0, 12, 55, 16);
+ let datetime_utc_4 = DatetimeUtc::from_ymd_and_hms(1970, 11, 31, 12, 55, 16);
+ let datetime_utc_5 = DatetimeUtc::from_ymd_and_hms(1971, 2, 29, 12, 55, 16);
+ let datetime_utc_6 = DatetimeUtc::from_ymd_and_hms(1970, 11, 23, 25, 0, 0);
+ let datetime_utc_7 = DatetimeUtc::from_ymd_and_hms(1970, 11, 23, 0, 61, 0);
+ let datetime_utc_8 = DatetimeUtc::from_ymd_and_hms(1970, 11, 23, 0, 0, 61);
+
+ assert!(datetime_utc_1.is_err());
+ assert_eq!(datetime_utc_1.unwrap_err().to_string(), "failed to construct datetime from ymd 1970-00-23");
+ assert!(datetime_utc_2.is_err());
+ assert_eq!(datetime_utc_2.unwrap_err().to_string(), "failed to construct datetime from ymd 1970-13-23");
+ assert!(datetime_utc_3.is_err());
+ assert_eq!(datetime_utc_3.unwrap_err().to_string(), "failed to construct datetime from ymd 1970-11-00");
+ assert!(datetime_utc_4.is_err());
+ assert_eq!(datetime_utc_4.unwrap_err().to_string(), "failed to construct datetime from ymd 1970-11-31");
+ assert!(datetime_utc_5.is_err());
+ assert_eq!(datetime_utc_5.unwrap_err().to_string(), "failed to construct datetime from ymd 1971-02-29");
+ assert!(datetime_utc_6.is_err());
+ assert_eq!(datetime_utc_6.unwrap_err().to_string(), "failed to construct datetime from hms 25:00:00");
+ assert!(datetime_utc_7.is_err());
+ assert_eq!(datetime_utc_7.unwrap_err().to_string(), "failed to construct datetime from hms 00:61:00");
+ assert!(datetime_utc_8.is_err());
+ assert_eq!(datetime_utc_8.unwrap_err().to_string(), "failed to construct datetime from hms 00:00:61");
+ }
+
+ #[test]
+ fn when_from_string_then_ymd_and_hms_are_as_expected() {
+ let date_utc: Result<DatetimeUtc> = "1970-11-23T12:55:16.000Z".parse();
+
+ assert!(date_utc.is_ok());
+ let datetime_utc = date_utc.unwrap();
+ assert_eq!(datetime_utc.year(), 1970);
+ assert_eq!(datetime_utc.month(), 11);
+ assert_eq!(datetime_utc.day(), 23);
+ assert_eq!(datetime_utc.hour(), 12);
+ assert_eq!(datetime_utc.minute(), 55);
+ assert_eq!(datetime_utc.second(), 16);
+ }
+
+ #[test]
+ fn when_from_string_then_to_string_as_expected() {
+ let datetime_utc: Result<DatetimeUtc> = "1970-11-23T12:55:16.000Z".parse();
+
+ assert!(datetime_utc.is_ok());
+ assert_eq!(datetime_utc.unwrap().to_string(), "1970-11-23T12:55:16.000Z")
+ }
+
+ #[test]
+ fn when_from_invalid_string_then_err() {
+ let datetime_utc_1: Result<DatetimeUtc> = "1970-00-23T00:00:00.000Z".parse();
+ let datetime_utc_2: Result<DatetimeUtc> = "1970-13-23T00:00:00.000Z".parse();
+ let datetime_utc_3: Result<DatetimeUtc> = "1970-11-0T00:00:00.000Z".parse();
+ let datetime_utc_4: Result<DatetimeUtc> = "1970-11-31T00:00:00.000Z".parse();
+ let datetime_utc_5: Result<DatetimeUtc> = "1971-02-29T00:00:00.000Z".parse();
+ let datetime_utc_6: Result<DatetimeUtc> = "1970-11-23T25:00:00.000Z".parse();
+ let datetime_utc_7: Result<DatetimeUtc> = "1970-11-23T00:61:00.000Z".parse();
+ let datetime_utc_8: Result<DatetimeUtc> = "1970-11-23T00:00:61.000Z".parse();
+
+ assert!(datetime_utc_1.is_err());
+ assert!(datetime_utc_2.is_err());
+ assert!(datetime_utc_3.is_err());
+ assert!(datetime_utc_4.is_err());
+ assert!(datetime_utc_5.is_err());
+ assert!(datetime_utc_6.is_err());
+ assert!(datetime_utc_7.is_err());
+ assert!(datetime_utc_8.is_err());
+ }
+}
diff --git a/backend/core/src/models/mod.rs b/backend/core/src/models/mod.rs
index b790fcb..2eb7ac2 100644
--- a/backend/core/src/models/mod.rs
+++ b/backend/core/src/models/mod.rs
@@ -5,6 +5,7 @@ pub mod category;
pub mod category_balance;
pub mod category_budget;
pub mod category_transfer;
-pub mod date_time_utc;
+pub mod date_utc;
+pub mod datetime_utc;
pub mod transaction;
pub mod transaction_categorisation;
diff --git a/backend/core/src/models/transaction.rs b/backend/core/src/models/transaction.rs
index edfabce..18a4c0e 100644
--- a/backend/core/src/models/transaction.rs
+++ b/backend/core/src/models/transaction.rs
@@ -3,7 +3,7 @@ use diesel::prelude::*;
use crate::models::account::Account;
-use super::date_time_utc::DateTimeUtc;
+use super::datetime_utc::DatetimeUtc;
#[derive(Builder, Queryable, Identifiable, Selectable, Insertable, Associations, Debug, PartialEq)]
#[diesel(table_name = crate::schema::transactions)]
@@ -13,6 +13,6 @@ pub struct Transaction {
pub description: String,
pub payee: String,
pub quantity: i32,
- pub date: DateTimeUtc,
+ pub date: DatetimeUtc,
pub account_id: i32,
}
diff --git a/backend/core/src/queries/budget_updates/get_first_with_date.rs b/backend/core/src/queries/budget_updates/get_first_with_date.rs
index 1df3ae2..0d5b2e1 100644
--- a/backend/core/src/queries/budget_updates/get_first_with_date.rs
+++ b/backend/core/src/queries/budget_updates/get_first_with_date.rs
@@ -1,22 +1,23 @@
use anyhow::{Context, Result};
use crate::{
- models::budget_update::BudgetUpdate,
+ models::{budget_update::BudgetUpdate, date_utc::DateUtc},
schema::budget_updates::{
self as budget_updates_schema, dsl::budget_updates as budget_updates_table,
},
};
use diesel::{
- ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection,
+ ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection
};
pub fn get_first_budget_update_with_date(
- date: &str,
+ date: &DateUtc,
connection: &mut SqliteConnection,
-) -> Result<BudgetUpdate> {
+) -> Result<Option<BudgetUpdate>> {
let first_budget_update = budget_updates_table
.select(BudgetUpdate::as_select())
- .filter(budget_updates_schema::date.eq(date))
+ .filter(budget_updates_schema::date.eq(date.to_string()))
.first(connection)
- .with_context(|| format!("failed to get first budget update with date {}", date))?;
+ .optional()
+ .with_context(|| format!("failed to get first budget update with date {}", date.to_string()))?;
Ok(first_budget_update)
}
diff --git a/backend/core/src/queries/budget_updates/get_most_recent_with_category_id.rs b/backend/core/src/queries/budget_updates/get_most_recent_with_category_id.rs
index 4e13c50..41ef5ec 100644
--- a/backend/core/src/queries/budget_updates/get_most_recent_with_category_id.rs
+++ b/backend/core/src/queries/budget_updates/get_most_recent_with_category_id.rs
@@ -1,20 +1,20 @@
use anyhow::{Context, Result};
use crate::{
- models::date_time_utc::DateTimeUtc,
+ models::date_utc::DateUtc,
schema::budget_updates::{
self as budget_updates_schema, dsl::budget_updates as budget_updates_table,
- },
+ }, traits::nowlike::Nowlike,
};
use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection};
pub fn get_most_recent_budget_update_date_with_category_id(
category_id: i32,
connection: &mut SqliteConnection,
-) -> Result<Option<String>> {
+) -> Result<Option<DateUtc>> {
let most_recent_date = budget_updates_table
.select(max(budget_updates_schema::date))
.filter(budget_updates_schema::category_id.eq(category_id))
- .filter(budget_updates_schema::date.le(DateTimeUtc::now().to_day_str()))
+ .filter(budget_updates_schema::date.le(DateUtc::now()))
.first(connection)
.with_context(|| format!("failed to get most recent budget update date with category ID {}", category_id))?;
Ok(most_recent_date)
diff --git a/backend/core/src/queries/categories/get_all_balances.rs b/backend/core/src/queries/categories/get_all_balances.rs
index da4b0cd..c9a0102 100644
--- a/backend/core/src/queries/categories/get_all_balances.rs
+++ b/backend/core/src/queries/categories/get_all_balances.rs
@@ -35,7 +35,7 @@ fn get_all_category_balances_without_context(
&category_transfer_from_sums,
&category_transfer_to_sums,
&budget_updates,
- );
+ )?;
balances.push(CategoryBalance {
category_id: category.id,
diff --git a/backend/core/src/queries/categories/get_most_recent_budget_per_category_id.rs b/backend/core/src/queries/categories/get_most_recent_budget_per_category_id.rs
index b8f1a76..fc0a5e1 100644
--- a/backend/core/src/queries/categories/get_most_recent_budget_per_category_id.rs
+++ b/backend/core/src/queries/categories/get_most_recent_budget_per_category_id.rs
@@ -31,11 +31,15 @@ fn get_most_recent_category_budget_per_category_id_without_context(
});
continue;
}
+ let most_recent_budget_update_date = most_recent_budget_update_date.unwrap();
let most_recent_budget_update = get_first_budget_update_with_date(
- most_recent_budget_update_date.unwrap().as_str(),
+ &most_recent_budget_update_date,
connection,
)?;
+ // The fact we found the date by getting the most recent budget update
+ // date guarantees that the Option is Some
+ let most_recent_budget_update = most_recent_budget_update.unwrap();
budgets.push(CategoryBudget {
category_id,
diff --git a/backend/core/src/traits/dateable.rs b/backend/core/src/traits/dateable.rs
new file mode 100644
index 0000000..05f2d74
--- /dev/null
+++ b/backend/core/src/traits/dateable.rs
@@ -0,0 +1,5 @@
+pub trait Dateable {
+ fn year(&self) -> i32;
+ fn month(&self) -> u32;
+ fn day(&self) -> u32;
+}
diff --git a/backend/core/src/traits/mod.rs b/backend/core/src/traits/mod.rs
new file mode 100644
index 0000000..ab5caca
--- /dev/null
+++ b/backend/core/src/traits/mod.rs
@@ -0,0 +1,3 @@
+pub mod dateable;
+pub mod nowlike;
+pub mod timeable;
diff --git a/backend/core/src/traits/nowlike.rs b/backend/core/src/traits/nowlike.rs
new file mode 100644
index 0000000..bae2309
--- /dev/null
+++ b/backend/core/src/traits/nowlike.rs
@@ -0,0 +1,3 @@
+pub trait Nowlike {
+ fn now() -> Self;
+}
diff --git a/backend/core/src/traits/timeable.rs b/backend/core/src/traits/timeable.rs
new file mode 100644
index 0000000..f6f9244
--- /dev/null
+++ b/backend/core/src/traits/timeable.rs
@@ -0,0 +1,5 @@
+pub trait Timeable {
+ fn hour(&self) -> u32;
+ fn minute(&self) -> u32;
+ fn second(&self) -> u32;
+}
diff --git a/backend/core/src/utils/calculate_budgets_accrual.rs b/backend/core/src/utils/calculate_budgets_accrual.rs
index 22061ee..56171ca 100644
--- a/backend/core/src/utils/calculate_budgets_accrual.rs
+++ b/backend/core/src/utils/calculate_budgets_accrual.rs
@@ -1,10 +1,9 @@
-use crate::models::{
- budget_update::BudgetUpdate,
- category::Category,
- date_time_utc::DateTimeUtc
-};
+use anyhow::Result;
+use crate::{models::{
+ budget_update::BudgetUpdate, category::Category, date_utc::DateUtc
+}, traits::nowlike::Nowlike};
-pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> i64 {
+pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUpdate]) -> Result<i64> {
let mut budget_updates = budget_updates
.iter()
.filter(|bu| bu.category_id == category.id)
@@ -13,28 +12,30 @@ pub fn calculate_budgets_accrual(category: &Category, budget_updates: &[BudgetUp
budget_updates.reverse();
let mut budgets_accrual = 0.0_f64;
- let mut calculated_back_to_date = DateTimeUtc::now();
+ let mut calculated_back_to_date = DateUtc::now();
for budget_update in budget_updates {
let days_on_this_budget = calculated_back_to_date.day_diff(&budget_update.date);
if days_on_this_budget == 0_u32 {
- continue;
+ return Err(anyhow::Error::msg(format!(
+ "failed to calculate budgets accrual since two budget updates had the same date ({})",
+ budget_update.date,
+ )));
}
let budget_per_day = budget_update.new_budget as f64 / budget_update.new_period as f64;
budgets_accrual += days_on_this_budget as f64 * budget_per_day;
calculated_back_to_date = budget_update.date.clone();
}
- budgets_accrual.floor() as i64
+ Ok(budgets_accrual.floor() as i64)
}
#[cfg(test)]
mod test {
- use chrono::{Days, Timelike, Utc};
+ use chrono::{Days, Utc};
use crate::{
- models::{budget_update::BudgetUpdate, category::Category, date_time_utc::DateTimeUtc},
- utils::calculate_budgets_accrual,
+ models::{budget_update::BudgetUpdate, category::Category, date_utc::DateUtc}, traits::nowlike::Nowlike, utils::calculate_budgets_accrual
};
#[test]
@@ -47,7 +48,8 @@ mod test {
let result = calculate_budgets_accrual(&category, &budget_updates);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -59,14 +61,15 @@ mod test {
let budget_updates = [BudgetUpdate {
id: 0,
category_id: 1,
- date: DateTimeUtc::now(),
+ date: DateUtc::now(),
new_budget: 100,
new_period: 1,
}];
let result = calculate_budgets_accrual(&category, &budget_updates);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -78,14 +81,15 @@ mod test {
let budget_updates = [BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ date: DateUtc::from(Utc::now().checked_sub_days(Days::new(120)).unwrap()),
new_budget: 100,
new_period: 1,
}];
let result = calculate_budgets_accrual(&category, &budget_updates);
- assert_eq!(result, 12_000);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 12_000);
}
#[test]
@@ -98,14 +102,14 @@ mod test {
BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(&Utc::now().checked_sub_days(Days::new(60)).unwrap()),
+ date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(60)).unwrap()),
new_budget: 200,
new_period: 1,
},
BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
new_budget: 100,
new_period: 1,
},
@@ -113,11 +117,12 @@ mod test {
let result = calculate_budgets_accrual(&category, &budget_updates);
- assert_eq!(result, 18_000);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 18_000);
}
#[test]
- fn when_two_budget_updates_on_same_day_then_uses_latest_only() {
+ fn when_two_budget_updates_on_same_day_then_returns_err() {
let category = Category {
id: 0,
name: String::from("Groceries"),
@@ -126,26 +131,14 @@ mod test {
BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(
- &Utc::now()
- .checked_sub_days(Days::new(120))
- .unwrap()
- .with_hour(13)
- .unwrap(),
- ),
+ date: DateUtc::from_ymd(2020, 12, 25).unwrap(),
new_budget: 200,
new_period: 1,
},
BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(
- &Utc::now()
- .checked_sub_days(Days::new(120))
- .unwrap()
- .with_hour(12)
- .unwrap(),
- ),
+ date: DateUtc::from_ymd(2020, 12, 25).unwrap(),
new_budget: 100,
new_period: 1,
},
@@ -156,7 +149,7 @@ mod test {
let result_leftways = calculate_budgets_accrual(&category, &budget_updates);
let result_rightways = calculate_budgets_accrual(&category, &budget_updates_reversed);
- assert_eq!(result_leftways, 24_000);
- assert_eq!(result_rightways, 24_000);
+ assert!(result_leftways.is_err());
+ assert!(result_rightways.is_err());
}
}
diff --git a/backend/core/src/utils/calculate_category_balance.rs b/backend/core/src/utils/calculate_category_balance.rs
index e461cee..296b9de 100644
--- a/backend/core/src/utils/calculate_category_balance.rs
+++ b/backend/core/src/utils/calculate_category_balance.rs
@@ -1,3 +1,4 @@
+use anyhow::Result;
use crate::{
models::{budget_update::BudgetUpdate, category::Category},
utils::{calculate_budgets_accrual, find_by_id_or},
@@ -9,13 +10,18 @@ pub fn calculate_category_balance(
category_transfer_from_sums: &[(i32, Option<i64>)],
category_transfer_to_sums: &[(i32, Option<i64>)],
budget_updates: &[BudgetUpdate],
-) -> i64 {
+) -> Result<i64> {
let transaction_sum = find_by_id_or(transaction_sums, category.id, 0);
let category_transfer_from_sum = find_by_id_or(category_transfer_from_sums, category.id, 0);
let category_transfer_to_sum = find_by_id_or(category_transfer_to_sums, category.id, 0);
- let budget_accruals_sum = calculate_budgets_accrual(category, budget_updates);
-
- transaction_sum + budget_accruals_sum + category_transfer_to_sum - category_transfer_from_sum
+ let budget_accruals_sum = calculate_budgets_accrual(category, budget_updates)?;
+
+ Ok(
+ transaction_sum
+ + budget_accruals_sum
+ + category_transfer_to_sum
+ - category_transfer_from_sum
+ )
}
#[cfg(test)]
@@ -23,7 +29,7 @@ mod test {
use chrono::{Days, Utc};
use crate::{
- models::{budget_update::BudgetUpdate, category::Category, date_time_utc::DateTimeUtc},
+ models::{budget_update::BudgetUpdate, category::Category, date_utc::DateUtc},
utils::calculate_category_balance
};
@@ -36,7 +42,8 @@ mod test {
let result = calculate_category_balance(&category, &[], &[], &[], &[]);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -51,7 +58,7 @@ mod test {
let budget_updates = [BudgetUpdate {
id: 0,
category_id: 1,
- date: DateTimeUtc::new(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ date: DateUtc::from(Utc::now().checked_sub_days(Days::new(120)).unwrap()),
new_budget: 77,
new_period: 1,
}];
@@ -64,7 +71,8 @@ mod test {
&budget_updates,
);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -86,7 +94,8 @@ mod test {
&budget_updates,
);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -108,7 +117,8 @@ mod test {
&budget_updates,
);
- assert_eq!(result, 0_i64);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 0_i64);
}
#[test]
@@ -123,7 +133,7 @@ mod test {
let budget_updates = [BudgetUpdate {
id: 0,
category_id: 0,
- date: DateTimeUtc::new(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
+ date: DateUtc::from(&Utc::now().checked_sub_days(Days::new(120)).unwrap()),
new_budget: 77,
new_period: 1,
}];
@@ -137,6 +147,7 @@ mod test {
);
let expected_result = 100 + (-91) + 79 + 77 * 120;
- assert_eq!(result, expected_result);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), expected_result);
}
}
diff --git a/backend/core/tests/common/fakes/account.rs b/backend/core/tests/common/fakes/account.rs
index e915aed..38422bf 100644
--- a/backend/core/tests/common/fakes/account.rs
+++ b/backend/core/tests/common/fakes/account.rs
@@ -1,10 +1,10 @@
-use budgeting_app_core::models::{account::Account, date_time_utc::DateTimeUtc};
+use budgeting_app_core::{models::{account::Account, datetime_utc::DatetimeUtc}, traits::nowlike::Nowlike};
pub fn make_fake_account(id: i32) -> Account {
Account {
id,
name: String::from("Joe Bloggs"),
opening_balance: 0,
- opening_date: DateTimeUtc::now(),
+ opening_date: DatetimeUtc::now(),
}
}
diff --git a/backend/core/tests/common/fakes/budget_update.rs b/backend/core/tests/common/fakes/budget_update.rs
index e7ee9b9..f27360f 100644
--- a/backend/core/tests/common/fakes/budget_update.rs
+++ b/backend/core/tests/common/fakes/budget_update.rs
@@ -1,4 +1,4 @@
-use budgeting_app_core::models::{budget_update::{BudgetUpdate, BudgetUpdateBuilder}, date_time_utc::DateTimeUtc};
+use budgeting_app_core::{models::{budget_update::{BudgetUpdate, BudgetUpdateBuilder}, date_utc::DateUtc}, traits::nowlike::Nowlike};
pub fn make_fake_budget_update(id: i32) -> BudgetUpdate {
make_fake_budget_update_builder(id).build().unwrap()
@@ -9,7 +9,7 @@ pub fn make_fake_budget_update_builder(id: i32) -> BudgetUpdateBuilder {
builder
.id(id)
.category_id(0)
- .date(DateTimeUtc::now())
+ .date(DateUtc::now())
.new_budget(0)
.new_period(0);
builder
diff --git a/backend/core/tests/common/fakes/transaction.rs b/backend/core/tests/common/fakes/transaction.rs
index d113ce5..88a9a71 100644
--- a/backend/core/tests/common/fakes/transaction.rs
+++ b/backend/core/tests/common/fakes/transaction.rs
@@ -1,4 +1,4 @@
-use budgeting_app_core::models::{date_time_utc::DateTimeUtc, transaction::{Transaction, TransactionBuilder}};
+use budgeting_app_core::{models::{datetime_utc::DatetimeUtc, transaction::{Transaction, TransactionBuilder}}, traits::nowlike::Nowlike};
pub fn make_fake_transaction(id: i32) -> Transaction {
make_fake_transaction_builder(id).build().unwrap()
@@ -11,7 +11,7 @@ pub fn make_fake_transaction_builder(id: i32) -> TransactionBuilder {
.description(String::from("Fake category transfer"))
.payee(String::from("Fake payee"))
.quantity(0)
- .date(DateTimeUtc::now())
+ .date(DatetimeUtc::now())
.account_id(0);
builder
}
diff --git a/backend/core/tests/models/date_time_utc.rs b/backend/core/tests/models/date_time_utc.rs
deleted file mode 100644
index 1f86968..0000000
--- a/backend/core/tests/models/date_time_utc.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use anyhow::Result;
-use budgeting_app_core::models::date_time_utc::DateTimeUtc;
-use diesel::{sql_query, Connection, ExpressionMethods, QueryDsl, RunQueryDsl, Selectable, SelectableHelper, SqliteConnection};
-use diesel::prelude::{Insertable, Queryable};
-
-use crate::common::test_context::TestContext;
-
-diesel::table! {
- date_time_utc_test (id) {
- id -> Integer,
- date_time_utc -> Text,
- }
-}
-
-#[derive(Insertable, Queryable, Selectable, Clone)]
-#[diesel(table_name = date_time_utc_test)]
-pub struct DateTimeUtcTest {
- pub id: i32,
- pub date_time_utc: DateTimeUtc,
-}
-
-fn new_test_context() -> Result<TestContext> {
- let test_context = TestContext::new();
- let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
- let query = "CREATE TABLE date_time_utc_test(id INTEGER PRIMARY KEY, date_time_utc TEXT);";
- sql_query(query).execute(connection)?;
- Ok(test_context)
-}
-
-fn insert_date_time_utc(id: i32, date_time_utc: &DateTimeUtc, connection: &mut SqliteConnection) -> Result<usize> {
- let model = DateTimeUtcTest {
- id,
- date_time_utc: date_time_utc.clone(),
- };
- let num_rows_inserted = diesel::insert_into(date_time_utc_test::dsl::date_time_utc_test)
- .values(&[model])
- .execute(connection)?;
- Ok(num_rows_inserted)
-}
-
-fn get_date_time_utc(id: i32, connection: &mut SqliteConnection) -> Result<DateTimeUtcTest> {
- let date_time_utc: Vec<DateTimeUtcTest> = date_time_utc_test::dsl::date_time_utc_test
- .select(DateTimeUtcTest::as_select())
- .filter(date_time_utc_test::id.eq(id))
- .load(connection)?;
- Ok(date_time_utc[0].clone())
-}
-
-#[test]
-fn when_insert_date_time_utc_and_select_then_returns_original_value() -> Result<()> {
- let date_time_utc = DateTimeUtc::now();
- let test_context = new_test_context()?;
- let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
- let num_rows_inserted = insert_date_time_utc(0, &date_time_utc, connection)?;
- assert_eq!(num_rows_inserted, 1);
- let date_time_utc_returned = get_date_time_utc(0, connection)?;
- assert_eq!(date_time_utc_returned.date_time_utc.to_date_str(), date_time_utc.to_date_str());
- Ok(())
-}
diff --git a/backend/core/tests/models/date_utc.rs b/backend/core/tests/models/date_utc.rs
new file mode 100644
index 0000000..40f8c36
--- /dev/null
+++ b/backend/core/tests/models/date_utc.rs
@@ -0,0 +1,60 @@
+use anyhow::Result;
+use budgeting_app_core::models::date_utc::DateUtc;
+use budgeting_app_core::traits::nowlike::Nowlike;
+use diesel::{sql_query, Connection, ExpressionMethods, QueryDsl, RunQueryDsl, Selectable, SelectableHelper, SqliteConnection};
+use diesel::prelude::{Insertable, Queryable};
+
+use crate::common::test_context::TestContext;
+
+diesel::table! {
+ date_utc_test (id) {
+ id -> Integer,
+ date_utc -> Text,
+ }
+}
+
+#[derive(Insertable, Queryable, Selectable, Clone)]
+#[diesel(table_name = date_utc_test)]
+pub struct DateUtcTest {
+ pub id: i32,
+ pub date_utc: DateUtc,
+}
+
+fn new_test_context() -> Result<TestContext> {
+ let test_context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
+ let query = "CREATE TABLE date_utc_test(id INTEGER PRIMARY KEY, date_utc TEXT);";
+ sql_query(query).execute(connection)?;
+ Ok(test_context)
+}
+
+fn insert_date_utc(id: i32, date_utc: &DateUtc, connection: &mut SqliteConnection) -> Result<usize> {
+ let model = DateUtcTest {
+ id,
+ date_utc: date_utc.clone(),
+ };
+ let num_rows_inserted = diesel::insert_into(date_utc_test::dsl::date_utc_test)
+ .values(&[model])
+ .execute(connection)?;
+ Ok(num_rows_inserted)
+}
+
+fn get_date_utc(id: i32, connection: &mut SqliteConnection) -> Result<DateUtcTest> {
+ let date_utc: Vec<DateUtcTest> = date_utc_test::dsl::date_utc_test
+ .select(DateUtcTest::as_select())
+ .filter(date_utc_test::id.eq(id))
+ .load(connection)?;
+ Ok(date_utc[0].clone())
+}
+
+#[test]
+fn when_insert_date_utc_and_select_then_returns_original_value() -> Result<()> {
+ let date_utc = DateUtc::now();
+ let test_context = new_test_context()?;
+ let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
+ let num_rows_inserted = insert_date_utc(0, &date_utc, connection)?;
+ assert_eq!(num_rows_inserted, 1);
+ let date_utc_returned = get_date_utc(0, connection)?;
+ assert_eq!(date_utc_returned.date_utc, date_utc);
+ Ok(())
+}
diff --git a/backend/core/tests/models/datetime_utc.rs b/backend/core/tests/models/datetime_utc.rs
new file mode 100644
index 0000000..7029429
--- /dev/null
+++ b/backend/core/tests/models/datetime_utc.rs
@@ -0,0 +1,60 @@
+use anyhow::Result;
+use budgeting_app_core::models::datetime_utc::DatetimeUtc;
+use budgeting_app_core::traits::nowlike::Nowlike;
+use diesel::{sql_query, Connection, ExpressionMethods, QueryDsl, RunQueryDsl, Selectable, SelectableHelper, SqliteConnection};
+use diesel::prelude::{Insertable, Queryable};
+
+use crate::common::test_context::TestContext;
+
+diesel::table! {
+ datetime_utc_test (id) {
+ id -> Integer,
+ datetime_utc -> Text,
+ }
+}
+
+#[derive(Insertable, Queryable, Selectable, Clone)]
+#[diesel(table_name = datetime_utc_test)]
+pub struct DatetimeUtcTest {
+ pub id: i32,
+ pub datetime_utc: DatetimeUtc,
+}
+
+fn new_test_context() -> Result<TestContext> {
+ let test_context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
+ let query = "CREATE TABLE datetime_utc_test(id INTEGER PRIMARY KEY, datetime_utc TEXT);";
+ sql_query(query).execute(connection)?;
+ Ok(test_context)
+}
+
+fn insert_datetime_utc(id: i32, datetime_utc: &DatetimeUtc, connection: &mut SqliteConnection) -> Result<usize> {
+ let model = DatetimeUtcTest {
+ id,
+ datetime_utc: datetime_utc.clone(),
+ };
+ let num_rows_inserted = diesel::insert_into(datetime_utc_test::dsl::datetime_utc_test)
+ .values(&[model])
+ .execute(connection)?;
+ Ok(num_rows_inserted)
+}
+
+fn get_datetime_utc(id: i32, connection: &mut SqliteConnection) -> Result<DatetimeUtcTest> {
+ let datetime_utc: Vec<DatetimeUtcTest> = datetime_utc_test::dsl::datetime_utc_test
+ .select(DatetimeUtcTest::as_select())
+ .filter(datetime_utc_test::id.eq(id))
+ .load(connection)?;
+ Ok(datetime_utc[0].clone())
+}
+
+#[test]
+fn when_insert_datetime_utc_and_select_then_returns_original_value() -> Result<()> {
+ let datetime_utc = DatetimeUtc::now();
+ let test_context = new_test_context()?;
+ let connection = &mut SqliteConnection::establish(&test_context.db_url)?;
+ let num_rows_inserted = insert_datetime_utc(0, &datetime_utc, connection)?;
+ assert_eq!(num_rows_inserted, 1);
+ let datetime_utc_returned = get_datetime_utc(0, connection)?;
+ assert_eq!(datetime_utc_returned.datetime_utc, datetime_utc);
+ Ok(())
+}
diff --git a/backend/core/tests/models/mod.rs b/backend/core/tests/models/mod.rs
index 5837dcd..d0d0e4a 100644
--- a/backend/core/tests/models/mod.rs
+++ b/backend/core/tests/models/mod.rs
@@ -1 +1,2 @@
-mod date_time_utc;
+mod date_utc;
+mod datetime_utc;
diff --git a/backend/core/tests/queries/budget_updates.rs b/backend/core/tests/queries/budget_updates.rs
index 147d06f..ae580e7 100644
--- a/backend/core/tests/queries/budget_updates.rs
+++ b/backend/core/tests/queries/budget_updates.rs
@@ -1,18 +1,28 @@
-use budgeting_app_core::queries;
+use budgeting_app_core::{models::date_utc::DateUtc, queries, traits::nowlike::Nowlike};
use diesel::{Connection, SqliteConnection};
use crate::common::{fakes::{budget_update::make_fake_budget_update_builder, category::make_fake_category}, test_context::TestContext};
+fn insert_prerequisite_data(connection: &mut SqliteConnection) {
+ let categories = [make_fake_category(0), make_fake_category(1)];
+ let _ = queries::categories::insert_categories(&categories, connection);
+}
+
#[test]
fn when_insert_budget_updates_then_returns_num_budget_updates() {
let context = TestContext::new();
let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
- let categories = [make_fake_category(0)];
+ insert_prerequisite_data(connection);
let budget_updates = [
- make_fake_budget_update_builder(0).category_id(0).build().unwrap(),
- make_fake_budget_update_builder(1).category_id(0).build().unwrap(),
+ make_fake_budget_update_builder(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .category_id(0)
+ .build().unwrap(),
+ make_fake_budget_update_builder(1)
+ .date(DateUtc::from_ymd(2020, 12, 26).unwrap())
+ .category_id(0)
+ .build().unwrap(),
];
- let _ = queries::categories::insert_categories(&categories, connection);
let num_budget_updates_inserted = queries::budget_updates::insert_budget_updates(&budget_updates, connection).unwrap();
@@ -20,20 +30,104 @@ fn when_insert_budget_updates_then_returns_num_budget_updates() {
}
#[test]
+fn given_budget_update_when_insert_budget_update_same_category_and_same_date_then_return_err() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+
+ let budget_updates_1 = [
+ make_fake_budget_update_builder(0)
+ .category_id(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .build().unwrap()
+ ];
+ let _ = queries::budget_updates::insert_budget_updates(&budget_updates_1, connection);
+
+ let budget_updates_2 = [
+ make_fake_budget_update_builder(1)
+ .category_id(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .build().unwrap()
+ ];
+ let result = queries::budget_updates::insert_budget_updates(&budget_updates_2, connection);
+
+ assert!(result.is_err());
+}
+
+#[test]
+fn given_budget_update_when_insert_budget_update_different_category_and_same_date_then_return_num_budget_updates_inserted() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+
+ let budget_updates_1 = [
+ make_fake_budget_update_builder(0)
+ .category_id(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .build().unwrap()
+ ];
+ let _ = queries::budget_updates::insert_budget_updates(&budget_updates_1, connection);
+
+ let budget_updates_2 = [
+ make_fake_budget_update_builder(1)
+ .category_id(1)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .build().unwrap()
+ ];
+ let result = queries::budget_updates::insert_budget_updates(&budget_updates_2, connection);
+
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 1);
+}
+
+#[test]
+fn given_budget_update_when_insert_budget_update_same_category_and_different_date_then_return_num_budget_updates_inserted() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+
+ let budget_updates_1 = [
+ make_fake_budget_update_builder(0)
+ .category_id(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .build().unwrap()
+ ];
+ let _ = queries::budget_updates::insert_budget_updates(&budget_updates_1, connection);
+
+ let budget_updates_2 = [
+ make_fake_budget_update_builder(1)
+ .category_id(0)
+ .date(DateUtc::from_ymd(2020, 12, 26).unwrap())
+ .build().unwrap()
+ ];
+ let result = queries::budget_updates::insert_budget_updates(&budget_updates_2, connection);
+
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), 1);
+}
+
+#[test]
fn given_budget_updates_when_get_all_budget_updates_then_return_budget_updates() {
let context = TestContext::new();
let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
- let categories = [make_fake_category(0)];
+ insert_prerequisite_data(connection);
let budget_updates = [
- make_fake_budget_update_builder(0).category_id(0).build().unwrap(),
- make_fake_budget_update_builder(1).category_id(0).build().unwrap(),
+ make_fake_budget_update_builder(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .category_id(0)
+ .build().unwrap(),
+ make_fake_budget_update_builder(1)
+ .date(DateUtc::from_ymd(2020, 12, 26).unwrap())
+ .category_id(0)
+ .build().unwrap(),
];
- let _ = queries::categories::insert_categories(&categories, connection);
let _ = queries::budget_updates::insert_budget_updates(&budget_updates, connection).unwrap();
let budget_updates_in_db = queries::budget_updates::get_all_budget_updates(connection).unwrap();
assert_eq!(budget_updates_in_db.len(), budget_updates.len());
+ assert_eq!(budget_updates_in_db[0], budget_updates[0]);
+ assert_eq!(budget_updates_in_db[1], budget_updates[1]);
for budget_update in &budget_updates {
assert!(budget_updates_in_db.iter().any(|budget_update_in_db| budget_update_in_db.eq(&budget_update)));
}
@@ -46,12 +140,17 @@ fn given_budget_updates_when_get_all_budget_updates_then_return_budget_updates()
fn given_budget_updates_when_delete_all_budget_updates_then_return_num_of_budget_updates() {
let context = TestContext::new();
let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
- let categories = [make_fake_category(0)];
+ insert_prerequisite_data(connection);
let budget_updates = [
- make_fake_budget_update_builder(0).category_id(0).build().unwrap(),
- make_fake_budget_update_builder(1).category_id(0).build().unwrap(),
+ make_fake_budget_update_builder(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .category_id(0)
+ .build().unwrap(),
+ make_fake_budget_update_builder(1)
+ .date(DateUtc::from_ymd(2020, 12, 26).unwrap())
+ .category_id(0)
+ .build().unwrap(),
];
- let _ = queries::categories::insert_categories(&categories, connection);
let _ = queries::budget_updates::insert_budget_updates(&budget_updates, connection).unwrap();
let num_budget_updates_deleted = queries::budget_updates::delete_all_budget_updates(connection).unwrap();
@@ -63,12 +162,17 @@ fn given_budget_updates_when_delete_all_budget_updates_then_return_num_of_budget
fn given_deleted_budget_updates_when_get_all_budget_updates_then_return_empty() {
let context = TestContext::new();
let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
- let categories = [make_fake_category(0)];
+ insert_prerequisite_data(connection);
let budget_updates = [
- make_fake_budget_update_builder(0).category_id(0).build().unwrap(),
- make_fake_budget_update_builder(1).category_id(0).build().unwrap(),
+ make_fake_budget_update_builder(0)
+ .date(DateUtc::from_ymd(2020, 12, 25).unwrap())
+ .category_id(0)
+ .build().unwrap(),
+ make_fake_budget_update_builder(1)
+ .date(DateUtc::from_ymd(2020, 12, 26).unwrap())
+ .category_id(0)
+ .build().unwrap(),
];
- let _ = queries::categories::insert_categories(&categories, connection);
let _ = queries::budget_updates::insert_budget_updates(&budget_updates, connection).unwrap();
let _ = queries::budget_updates::delete_all_budget_updates(connection).unwrap();
@@ -76,3 +180,77 @@ fn given_deleted_budget_updates_when_get_all_budget_updates_then_return_empty()
assert_eq!(budget_updates_returned.len(), 0);
}
+
+#[test]
+fn given_no_budget_updates_when_get_first_with_date_then_return_none() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+
+ let first_with_date = queries::budget_updates::get_first_budget_update_with_date(&DateUtc::now(), connection).unwrap();
+
+ assert!(first_with_date.is_none());
+}
+
+#[test]
+fn given_budget_update_when_get_first_with_date_then_return_budget_update() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+ let date: DateUtc = "2024-10-28".parse().unwrap();
+ let budget_update = make_fake_budget_update_builder(0).date(date.clone()).build().unwrap();
+ let _ = queries::budget_updates::insert_budget_updates(&[budget_update.clone()], connection).unwrap();
+
+ let first_with_date = queries::budget_updates::get_first_budget_update_with_date(&date, connection).unwrap();
+
+ assert!(first_with_date.is_some());
+ assert_eq!(first_with_date.unwrap(), budget_update);
+}
+
+#[test]
+fn given_budget_update_when_get_first_with_different_date_then_return_none() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+ let date: DateUtc = "2024-10-28".parse().unwrap();
+ let different_date: DateUtc = "2024-10-29".parse().unwrap();
+ let budget_update = make_fake_budget_update_builder(0).date(date.clone()).build().unwrap();
+ let _ = queries::budget_updates::insert_budget_updates(&[budget_update.clone()], connection).unwrap();
+
+ let first_with_date = queries::budget_updates::get_first_budget_update_with_date(
+ &different_date,
+ connection
+ ).unwrap();
+
+ assert!(first_with_date.is_none());
+}
+
+#[test]
+fn given_many_budget_updates_when_get_first_with_date_then_return_any_with_date() {
+ let context = TestContext::new();
+ let connection = &mut SqliteConnection::establish(&context.db_url).unwrap();
+ insert_prerequisite_data(connection);
+ let budget_updates = [
+ make_fake_budget_update_builder(0).date("2024-12-25".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(1).date("2020-12-26".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(2).date("2020-12-27".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(3).date("2020-12-28".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(4).date("2020-12-29".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(5).date("2020-12-30".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(6).date("2020-12-31".parse().unwrap()).build().unwrap(),
+ make_fake_budget_update_builder(7).date("2021-01-01".parse().unwrap()).build().unwrap(),
+ ];
+ let _ = queries::budget_updates::insert_budget_updates(&budget_updates, connection).unwrap();
+
+ let first_with_date = queries::budget_updates::get_first_budget_update_with_date(
+ &"2024-12-25".parse().unwrap(),
+ connection,
+ ).unwrap();
+
+ assert!(first_with_date.is_some());
+ assert_eq!(first_with_date.unwrap(), budget_updates[0]);
+}
+
+#[test]
+fn todo() {
+ todo!("test get_most_recent_with_category_id");
+}
diff --git a/backend/core/tests/queries/categories.rs b/backend/core/tests/queries/categories.rs
index 917536e..45ffe8e 100644
--- a/backend/core/tests/queries/categories.rs
+++ b/backend/core/tests/queries/categories.rs
@@ -56,3 +56,18 @@ fn given_deleted_categories_when_get_all_categories_then_return_empty() {
assert_eq!(categories_returned.len(), 0);
}
+
+#[test]
+fn todo() {
+ todo!("test get_all_balances");
+}
+
+#[test]
+fn todo2() {
+ todo!("test get_all_ids");
+}
+
+#[test]
+fn todo3() {
+ todo!("test get_most_recent_per_category_id");
+}
diff --git a/backend/core/tests/queries/category_transfers.rs b/backend/core/tests/queries/category_transfers.rs
index d329d9c..80ec70f 100644
--- a/backend/core/tests/queries/category_transfers.rs
+++ b/backend/core/tests/queries/category_transfers.rs
@@ -76,3 +76,13 @@ fn given_deleted_category_transfers_when_get_all_category_transfers_then_return_
assert_eq!(category_transfers_returned.len(), 0);
}
+
+#[test]
+fn todo() {
+ todo!("test sum_quantity_per_from_category_id");
+}
+
+#[test]
+fn todo2() {
+ todo!("test sum_quantity_per_from_category_id");
+}
diff --git a/backend/core/tests/queries/transaction_categorisations.rs b/backend/core/tests/queries/transaction_categorisations.rs
index 3c2171d..7e0e742 100644
--- a/backend/core/tests/queries/transaction_categorisations.rs
+++ b/backend/core/tests/queries/transaction_categorisations.rs
@@ -84,3 +84,9 @@ fn given_deleted_transaction_categorisations_when_get_all_transaction_categorisa
assert_eq!(transaction_categorisations_returned.len(), 0);
}
+
+
+#[test]
+fn todo() {
+ todo!("test sum_quantity_per_category_id");
+}