From ee43de1c82f322169eacb4ec4444da227baea541 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 2 May 2025 16:37:30 +0200 Subject: [PATCH] Can import data from FinancesManager --- .../src/controllers/backup_controller.rs | 40 +++++++ moneymgr_backend/src/controllers/mod.rs | 1 + moneymgr_backend/src/converters.rs | 1 + .../converters/finances_manager_converter.rs | 107 ++++++++++++++++++ moneymgr_backend/src/lib.rs | 1 + moneymgr_backend/src/main.rs | 5 + 6 files changed, 155 insertions(+) create mode 100644 moneymgr_backend/src/controllers/backup_controller.rs create mode 100644 moneymgr_backend/src/converters.rs create mode 100644 moneymgr_backend/src/converters/finances_manager_converter.rs diff --git a/moneymgr_backend/src/controllers/backup_controller.rs b/moneymgr_backend/src/controllers/backup_controller.rs new file mode 100644 index 0000000..7b15192 --- /dev/null +++ b/moneymgr_backend/src/controllers/backup_controller.rs @@ -0,0 +1,40 @@ +use crate::controllers::HttpResult; +use crate::converters::finances_manager_converter::FinancesManagerFile; +use crate::extractors::auth_extractor::AuthExtractor; +use crate::extractors::file_extractor::FileExtractor; +use crate::models::accounts::AccountType; +use crate::services::accounts_service::UpdateAccountQuery; +use crate::services::movements_service::UpdateMovementQuery; +use crate::services::{accounts_service, movements_service}; +use actix_web::HttpResponse; + +/// Import data from a [FinancesManager](https://gitlab.com/pierre42100/cpp-financesmanager) file +pub async fn import_financesmanager(auth: AuthExtractor, file: FileExtractor) -> HttpResult { + let file = FinancesManagerFile::parse(&String::from_utf8_lossy(&file.buff))?; + + // Create each account & push the movements independently + for file_account in file.accounts { + let account = accounts_service::create( + auth.user_id(), + &UpdateAccountQuery { + name: file_account.name, + r#type: AccountType::Cash, + }, + ) + .await?; + + for file_movement in file_account.movements { + movements_service::create(&UpdateMovementQuery { + account_id: account.id(), + time: file_movement.time, + label: file_movement.label, + file_id: None, + amount: file_movement.amount, + checked: false, + }) + .await?; + } + } + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 21d305c..3f7a4ca 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -4,6 +4,7 @@ use std::error::Error; pub mod accounts_controller; pub mod auth_controller; +pub mod backup_controller; pub mod files_controller; pub mod movement_controller; pub mod server_controller; diff --git a/moneymgr_backend/src/converters.rs b/moneymgr_backend/src/converters.rs new file mode 100644 index 0000000..0baf476 --- /dev/null +++ b/moneymgr_backend/src/converters.rs @@ -0,0 +1 @@ +pub mod finances_manager_converter; diff --git a/moneymgr_backend/src/converters/finances_manager_converter.rs b/moneymgr_backend/src/converters/finances_manager_converter.rs new file mode 100644 index 0000000..8bba035 --- /dev/null +++ b/moneymgr_backend/src/converters/finances_manager_converter.rs @@ -0,0 +1,107 @@ +use std::num::{ParseFloatError, ParseIntError}; + +#[derive(thiserror::Error, Debug)] +enum FinancesManagerDecodeError { + #[error("Movement entry is not a three-part component! got {0} parts instead of 3!")] + MovementParts(usize), + #[error("Could not decode time!")] + Time(ParseIntError), + #[error("Could not decode amount!")] + Amount(ParseFloatError), +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct FinancesManagerMovement { + pub label: String, + pub time: u64, + pub amount: f32, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct FinancesManagerAccount { + pub name: String, + pub movements: Vec, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct FinancesManagerFile { + pub accounts: Vec, +} + +impl FinancesManagerFile { + /// Parse a finance manager file + pub fn parse(file: &str) -> anyhow::Result { + let mut res = Self { accounts: vec![] }; + + let mut curr_account = None; + + for l in file.lines() { + // Check if we reached the end of an account + if l.trim().is_empty() { + if let Some(a) = curr_account { + res.accounts.push(a); + } + curr_account = None; + continue; + } + + if l == "==============" { + continue; + } + + match &mut curr_account { + // Header + None => { + curr_account = Some(FinancesManagerAccount { + name: l.to_string(), + movements: vec![], + }); + } + + // Account content + Some(account) => { + let split = l.split(';').collect::>(); + + if split.len() != 3 { + return Err(FinancesManagerDecodeError::MovementParts(split.len()).into()); + } + + account.movements.push(FinancesManagerMovement { + label: split[1].to_string(), + time: split[0] + .parse::() + .map_err(FinancesManagerDecodeError::Time)?, + amount: split[2] + .parse::() + .map_err(FinancesManagerDecodeError::Amount)?, + }) + } + } + } + + // Push last account + + Ok(res) + } + + /// Encode FinancesManager file + pub fn encode(&self) -> String { + let mut out = String::new(); + + for account in &self.accounts { + out.push_str(&account.name); + out.push_str("\n==============\n"); + for movement in &account.movements { + out.push_str(&format!( + "{};{};{}\n", + movement.time, + movement.label.replace(';', ","), + movement.amount + )); + } + out.push_str("\n\n\n"); + } + + out + } +} diff --git a/moneymgr_backend/src/lib.rs b/moneymgr_backend/src/lib.rs index bd9f046..630c8a3 100644 --- a/moneymgr_backend/src/lib.rs +++ b/moneymgr_backend/src/lib.rs @@ -2,6 +2,7 @@ pub mod app_config; pub mod connections; pub mod constants; pub mod controllers; +pub mod converters; pub mod extractors; pub mod models; pub mod routines; diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index a4a3cd8..bbc6adf 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -161,6 +161,11 @@ async fn main() -> std::io::Result<()> { "/api/stats/balance_variation", web::get().to(stats_controller::balance_variation), ) + // Backup controller + .route( + "/api/backup/financesmanager/import", + web::post().to(backup_controller::import_financesmanager), + ) // Static assets .route("/", web::get().to(static_controller::root_index)) .route(