diff --git a/.sqlx/query-46e3c0b0e81ae0a98157ced83a6565d7ccf616a0a2b0d445f1be759779ff2fad.json b/.sqlx/query-46e3c0b0e81ae0a98157ced83a6565d7ccf616a0a2b0d445f1be759779ff2fad.json new file mode 100644 index 00000000..5ae0ff18 --- /dev/null +++ b/.sqlx/query-46e3c0b0e81ae0a98157ced83a6565d7ccf616a0a2b0d445f1be759779ff2fad.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE\n budgeting_userbudget AS c,\n budgeting_userbudget AS n\n SET n.amount = c.amount\n WHERE c.user_id = n.user_id\n AND c.year = ?\n AND n.year = ?\n AND c.amount != n.amount\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "46e3c0b0e81ae0a98157ced83a6565d7ccf616a0a2b0d445f1be759779ff2fad" +} diff --git a/api/src/database/budgeting/user_budget.rs b/api/src/database/budgeting/user_budget.rs index b0caab72..2ffbcda0 100644 --- a/api/src/database/budgeting/user_budget.rs +++ b/api/src/database/budgeting/user_budget.rs @@ -209,3 +209,29 @@ pub async fn insert_user_budget_into_db( let id = result.last_insert_id(); Ok(id) } + +#[tracing::instrument(name = "sync_user_budgets_in_db", skip(transaction))] +pub async fn sync_user_budgets_in_db( + transaction: &mut Transaction<'_, MySql>, +) -> Result { + let year = 2024; + let query = sqlx::query!( + r#" + UPDATE + budgeting_userbudget AS c, + budgeting_userbudget AS n + SET n.amount = c.amount + WHERE c.user_id = n.user_id + AND c.year = ? + AND n.year = ? + AND c.amount != n.amount + "#, + year, + year + 1 + ); + let result = transaction + .execute(query) + .await + .context("Failed to execute insert query")?; + Ok(result.rows_affected()) +} diff --git a/api/src/routes/budgeting/user_budget/mod.rs b/api/src/routes/budgeting/user_budget/mod.rs index e9cebe65..038076b2 100644 --- a/api/src/routes/budgeting/user_budget/mod.rs +++ b/api/src/routes/budgeting/user_budget/mod.rs @@ -12,6 +12,8 @@ mod modify; use modify::user_budget_modify; mod delete; use delete::user_budget_delete; +mod sync; +use sync::user_budget_sync; pub fn user_budgets_scope() -> Scope { scope("/userbudgets") @@ -21,6 +23,7 @@ pub fn user_budgets_scope() -> Scope { // TODO: what about PUT? .route("/{user_budget_id}/", patch().to(user_budget_modify)) .route("/{user_budget_id}/", delete().to(user_budget_delete)) + .route("/sync/", get().to(user_budget_sync)) } // TODO: wouldn't a general IdParam be better? diff --git a/api/src/routes/budgeting/user_budget/sync.rs b/api/src/routes/budgeting/user_budget/sync.rs new file mode 100644 index 00000000..6ee9fb5e --- /dev/null +++ b/api/src/routes/budgeting/user_budget/sync.rs @@ -0,0 +1,34 @@ +use crate::authorization::require_admin_user; +use crate::database::budgeting::user_budget::sync_user_budgets_in_db; +use crate::error::NormalApiError; +use actix_web::web::{Data, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::budgeting::UserBudgetSync; +use lrzcc_wire::user::{Project, User}; +use sqlx::MySqlPool; + +// TODO: write tests for this endpoint +#[tracing::instrument(name = "user_budget_sync")] +pub async fn user_budget_sync( + user: ReqData, + project: ReqData, + db_pool: Data, + // TODO: this can only be an authorization or unexpected error, we need a type for that +) -> Result { + require_admin_user(&user)?; + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let count = sync_user_budgets_in_db(&mut transaction).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok().content_type("application/json").json( + UserBudgetSync { + updated_budget_count: count as u32, + }, + )) +} diff --git a/cli/src/budgeting/user_budget.rs b/cli/src/budgeting/user_budget.rs index ef185ab1..992d8ff0 100644 --- a/cli/src/budgeting/user_budget.rs +++ b/cli/src/budgeting/user_budget.rs @@ -132,6 +132,9 @@ pub(crate) enum UserBudgetCommand { )] detail: bool, }, + + #[clap(about = "Sync user budgets of next year to those to this one")] + Sync, } pub(crate) use UserBudgetCommand::*; @@ -157,6 +160,7 @@ impl Execute for UserBudgetCommand { combined, detail, } => over(api, format, filter, *end, *combined, *detail), + Sync => sync(api, format), } } } @@ -262,3 +266,7 @@ fn over( (true, true) => print_object_list(request.combined_detail()?, format), } } + +fn sync(api: lrzcc::Api, format: Format) -> Result<(), Box> { + print_single_object(api.user_budget.sync()?, format) +} diff --git a/lib/src/budgeting/user_budget.rs b/lib/src/budgeting/user_budget.rs index 6c824784..978434a5 100644 --- a/lib/src/budgeting/user_budget.rs +++ b/lib/src/budgeting/user_budget.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, FixedOffset}; use lrzcc_wire::budgeting::{ UserBudget, UserBudgetCombined, UserBudgetCombinedDetail, UserBudgetCreateData, UserBudgetDetail, UserBudgetListParams, - UserBudgetModifyData, UserBudgetOver, + UserBudgetModifyData, UserBudgetOver, UserBudgetSync, }; use reqwest::blocking::Client; use reqwest::Url; @@ -354,4 +354,15 @@ impl UserBudgetApi { let url = format!("{}/over/", self.url); UserBudgetOverRequest::new(url.as_ref(), &self.client) } + + pub fn sync(&self) -> Result { + let url = format!("{}/sync/", self.url); + request( + &self.client, + Method::GET, + url.as_str(), + SerializableNone!(), + StatusCode::OK, + ) + } } diff --git a/wire/src/budgeting/user_budget.rs b/wire/src/budgeting/user_budget.rs index 086d26ab..d29c7519 100644 --- a/wire/src/budgeting/user_budget.rs +++ b/wire/src/budgeting/user_budget.rs @@ -109,3 +109,8 @@ pub struct UserBudgetCombinedDetail { pub user_cost: f64, pub user_budget: u32, } + +#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq)] +pub struct UserBudgetSync { + pub updated_budget_count: u32, +}