use std::{collections::HashMap, sync::Arc}; use axum::{extract::{Path, State}, Json}; use reqwest::{StatusCode}; use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel}; use serde::{Deserialize, Serialize}; use utoipa::IntoParams; use crate::{entities::{bal::{self, BalState}, book_instance, owner, prelude::*}, routes::auth::Claims, AppState}; #[derive(IntoParams)] #[into_params(names("id"), parameter_in = Path)] #[allow(dead_code)] pub struct BalByIdParams(u32); #[axum::debug_handler] #[utoipa::path( get, path = "/bal/{id}", params(BalByIdParams), security(("jwt" = [])), responses( (status = OK, body = bal::Model, description = "Found bal with corresponding ID in the database"), (status = NOT_FOUND, description = "No bal with this id exists in the database"), (status = FORBIDDEN, description = "You don't own the specified bal"), ), summary = "Get a bal by its ID", description = "Get a bal from its ID", tag = "bal-api", )] pub async fn get_bal_by_id( State(state): State>, claims: Claims, Path(id): Path, ) -> (StatusCode, Json>) { if let Ok(Some(res)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await { if !res.user_id == claims.user_id { (StatusCode::FORBIDDEN, Json(None)) } else { (StatusCode::OK, Json(Some(res))) } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BalCreateParams { name: String, start_timestamp: i64, end_timestamp: i64, } #[axum::debug_handler] #[utoipa::path( post, path = "/bal", request_body = BalCreateParams, security(("jwt" = [])), responses( (status = CREATED, body = bal::Model, description = "Successfully created BAL"), (status = BAD_REQUEST, body = bal::Model, description = "Time cannot go backwards in this world"), ), summary = "Create a new bal", description = "Create a new bal", tag = "bal-api", )] pub async fn create_bal( State(state): State>, claims: Claims, Json(instance_payload): Json, ) -> (StatusCode, Json>) { if instance_payload.start_timestamp > instance_payload.end_timestamp { return (StatusCode::BAD_REQUEST, Json(None)) } let bal = bal::ActiveModel { id: NotSet, user_id: Set(claims.user_id), name: Set(instance_payload.name), state: Set(BalState::Pending), start_timestamp: Set(instance_payload.start_timestamp), end_timestamp: Set(instance_payload.end_timestamp), }; let b = bal.save(state.db_conn.as_ref()).await; match b { Err(e) => { log::error!(target: "api", "Error while inserting new bal: {:#?}", e); (StatusCode::BAD_REQUEST, Json(None)) }, Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved")))) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BalUpdateParams { name: Option, start_timestamp: Option, end_timestamp: Option, } #[axum::debug_handler] #[utoipa::path( patch, path = "/bal/{id}", params(BalByIdParams), request_body = BalUpdateParams, security(("jwt" = [])), responses( (status = OK, body = bal::Model, description = "Successfully updated bal"), (status = NOT_FOUND, description = "No bal with specified id was found"), (status = FORBIDDEN, description = "You don't own the specified bal"), ), summary = "Update a bal", description = "Update a bal", tag = "bal-api", )] pub async fn update_bal( State(state): State>, claims: Claims, Path(id): Path, Json(payload): Json, ) -> (StatusCode, Json>) { if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await { if bal.user_id != claims.user_id { return (StatusCode::FORBIDDEN, Json(None)); } // Timestamp checks if (payload.start_timestamp.is_some() || payload.end_timestamp.is_some()) && payload.start_timestamp.unwrap_or(bal.start_timestamp) > payload.end_timestamp.unwrap_or(bal.end_timestamp) { return (StatusCode::BAD_REQUEST, Json(None)) } let mut bal: bal::ActiveModel = bal.into(); bal.name = match payload.name { None => bal.name, Some(v) => Set(v) }; bal.start_timestamp = match payload.start_timestamp { None => bal.start_timestamp, Some(v) => Set(v) }; bal.end_timestamp = match payload.end_timestamp { None => bal.end_timestamp, Some(v) => Set(v) }; match bal.update(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while updating bal from api: {:#?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) }, Ok(res) => { let model = res.try_into_model().expect("All fields should be set once the bal is saved"); (StatusCode::OK, Json(Some(model))) } } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[axum::debug_handler] #[utoipa::path( post, path = "/bal/{id}/start", params(BalByIdParams), security(("jwt" = [])), responses( (status = OK, body = bal::Model, description = "Successfully started bal"), (status = CONFLICT, description = "The specified BAL was not in a Pending state, cannot start it"), (status = CONFLICT, description = "Cannot have multiple ongoing BALs at the same time !"), (status = NOT_FOUND, description = "No bal with specified id was found"), (status = FORBIDDEN, description = "You don't own the specified bal"), ), summary = "Start a bal", description = "Start a bal", tag = "bal-api", )] pub async fn start_bal( State(state): State>, claims: Claims, Path(id): Path, ) -> (StatusCode, Json>) { if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await { if bal.user_id != claims.user_id { return (StatusCode::FORBIDDEN, Json(None)); } if bal.state != BalState::Pending { return (StatusCode::CONFLICT, Json(None)); } if bal::get_user_ongoing_bal(state.db_conn.as_ref(), claims.user_id).await.is_some() { return (StatusCode::CONFLICT, Json(None)); } let mut bal: bal::ActiveModel = bal.into(); bal.state = Set(BalState::Ongoing); match bal.update(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while updating bal from api: {:#?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) }, Ok(res) => { let model = res.try_into_model().expect("All fields should be set once the bal is saved"); (StatusCode::OK, Json(Some(model))) } } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[axum::debug_handler] #[utoipa::path( post, path = "/bal/{id}/stop", params(BalByIdParams), security(("jwt" = [])), responses( (status = OK, body = bal::Model, description = "Successfully stopped bal"), (status = CONFLICT, description = "The specified BAL was not in an Ongoing state, cannot stop it"), (status = NOT_FOUND, description = "No bal with specified id was found"), (status = FORBIDDEN, description = "You don't own the specified bal"), ), summary = "Stop a bal", description = "Stop a bal", tag = "bal-api", )] pub async fn stop_bal( State(state): State>, claims: Claims, Path(id): Path, ) -> (StatusCode, Json>) { if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await { if bal.user_id != claims.user_id { return (StatusCode::FORBIDDEN, Json(None)); } if bal.state != BalState::Ongoing { return (StatusCode::CONFLICT, Json(None)); } let mut bal: bal::ActiveModel = bal.into(); bal.state = Set(BalState::Ended); match bal.update(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while updating bal from api: {:#?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) }, Ok(res) => { let model = res.try_into_model().expect("All fields should be set once the bal is saved"); (StatusCode::OK, Json(Some(model))) } } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[axum::debug_handler] #[utoipa::path( get, path = "/bals", security(("jwt" = [])), responses( (status = OK, body = Vec, description = "All Bals you own"), ), summary = "Get all Bals you own", description = "Get all Bals you own", tag = "bal-api", )] pub async fn get_bals( State(state): State>, claims: Claims, ) -> (StatusCode, Json>) { if let Ok(res) = Bal::find().filter(bal::Column::UserId.eq(claims.user_id)).all(state.db_conn.as_ref()).await { (StatusCode::OK, Json(res)) } else { (StatusCode::NOT_FOUND, Json(vec![])) } } #[derive(Serialize, utoipa::ToSchema)] pub struct OwnerAccountingData { pub owed_money: f32, pub owed_books: Vec } #[derive(Serialize, utoipa::ToSchema)] pub struct BalAccounting { pub owner_map: HashMap, pub total_collected_money: f32 } #[axum::debug_handler] #[utoipa::path( get, path = "/bal/{id}/accounting", params(BalByIdParams), security(("jwt" = [])), responses( (status = OK, body = BalAccounting, description = "Accounting data of the specified BAL"), (status = NOT_FOUND, description = "No bal with this id exists in the database"), (status = FORBIDDEN, description = "You don't own the specified bal"), ), summary = "Get accounting data for specified BAL", description = "Get accounting data for specified BAL", tag = "bal-api", )] pub async fn get_bal_accounting( State(state): State>, claims: Claims, Path(id): Path ) -> (StatusCode, Json>) { if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await { if !bal.user_id == claims.user_id { (StatusCode::FORBIDDEN, Json(None)) } else { let mut accounting_data = BalAccounting { owner_map: HashMap::new(), total_collected_money: 0. }; let bal_owners = Owner::find() .filter(owner::Column::UserId.eq(claims.user_id)).all(state.db_conn.as_ref()).await.unwrap(); for owner in bal_owners { let owner_books = BookInstance::find() .filter(book_instance::Column::OwnerId.eq(owner.id)) .filter(book_instance::Column::BalId.eq(bal.id)) .all(state.db_conn.as_ref()).await.unwrap(); if owner_books.is_empty() { continue; } let mut owner_accounting_data = OwnerAccountingData { owed_money: 0., owed_books: vec![] }; for book_instance in owner_books { match book_instance.sold_price { Some(val) => owner_accounting_data.owed_money += val, None => owner_accounting_data.owed_books.push(book_instance), } } accounting_data.total_collected_money += owner_accounting_data.owed_money; accounting_data.owner_map.insert(owner.id, owner_accounting_data); } (StatusCode::OK, Json(Some(accounting_data))) } } else { (StatusCode::NOT_FOUND, Json(None)) } }