This commit is contained in:
parent
910b10de35
commit
b817d36816
6 changed files with 193 additions and 10 deletions
|
|
@ -34,6 +34,8 @@ where C: ConnectionTrait {
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
#[sea_orm(has_many = "super::book_instance::Entity")]
|
#[sea_orm(has_many = "super::book_instance::Entity")]
|
||||||
BookInstance,
|
BookInstance,
|
||||||
|
#[sea_orm(has_one = "super::bal_stats::Entity")]
|
||||||
|
BalStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::book_instance::Entity> for Entity {
|
impl Related<super::book_instance::Entity> for Entity {
|
||||||
|
|
|
||||||
38
src/entities/bal_stats.rs
Normal file
38
src/entities/bal_stats.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
#[sea_orm(table_name = "BALStats")]
|
||||||
|
#[schema(title="BalStats", as=entities::BALStats)]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = true)]
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
id: u32,
|
||||||
|
pub bal_id: u32,
|
||||||
|
|
||||||
|
pub total_owned_sold_books: u32,
|
||||||
|
pub total_sold_books: u32,
|
||||||
|
|
||||||
|
pub total_owned_collected_money: f32,
|
||||||
|
pub total_collected_money: f32,
|
||||||
|
|
||||||
|
pub total_different_owners: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to = "super::bal::Entity",
|
||||||
|
from = "Column::BalId",
|
||||||
|
to = "super::bal::Column::Id"
|
||||||
|
)]
|
||||||
|
Bal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Related<super::bal::Entity> for Entity {
|
||||||
|
fn to() -> RelationDef {
|
||||||
|
Relation::Bal.def()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod bal;
|
pub mod bal;
|
||||||
|
pub mod bal_stats;
|
||||||
pub mod book;
|
pub mod book;
|
||||||
pub mod book_instance;
|
pub mod book_instance;
|
||||||
pub mod owner;
|
pub mod owner;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
pub use super::bal::Entity as Bal;
|
pub use super::bal::Entity as Bal;
|
||||||
|
pub use super::bal_stats::Entity as BalStats;
|
||||||
pub use super::book::Entity as Book;
|
pub use super::book::Entity as Book;
|
||||||
pub use super::book_instance::Entity as BookInstance;
|
pub use super::book_instance::Entity as BookInstance;
|
||||||
pub use super::owner::Entity as Owner;
|
pub use super::owner::Entity as Owner;
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,8 @@ pub async fn run_server(db: Arc<DatabaseConnection>, port: u16, serve_docs: bool
|
||||||
.routes(routes!(routes::bal::stop_bal))
|
.routes(routes!(routes::bal::stop_bal))
|
||||||
.routes(routes!(routes::bal::get_bals))
|
.routes(routes!(routes::bal::get_bals))
|
||||||
.routes(routes!(routes::bal::get_bal_accounting))
|
.routes(routes!(routes::bal::get_bal_accounting))
|
||||||
|
.routes(routes!(routes::bal::return_to_owner))
|
||||||
|
.routes(routes!(routes::bal::get_stats_by_bal_id))
|
||||||
// Authentication
|
// Authentication
|
||||||
.route_layer(middleware::from_fn_with_state(shared_state.clone(), routes::auth::auth_middleware))
|
.route_layer(middleware::from_fn_with_state(shared_state.clone(), routes::auth::auth_middleware))
|
||||||
.routes(routes!(routes::auth::auth))
|
.routes(routes!(routes::auth::auth))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::{HashMap, HashSet}, sync::Arc};
|
||||||
|
|
||||||
use axum::{extract::{Path, State}, Json};
|
use axum::{extract::{Path, State}, Json};
|
||||||
use reqwest::{StatusCode};
|
use reqwest::{StatusCode};
|
||||||
|
|
@ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityT
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
use crate::{entities::{bal::{self, BalState}, book_instance, owner, prelude::*}, routes::auth::Claims, AppState};
|
use crate::{entities::{bal::{self, BalState}, bal_stats, book_instance, owner, prelude::*}, routes::auth::Claims, AppState};
|
||||||
|
|
||||||
|
|
||||||
#[derive(IntoParams)]
|
#[derive(IntoParams)]
|
||||||
|
|
@ -242,16 +242,49 @@ pub async fn stop_bal(
|
||||||
let mut bal: bal::ActiveModel = bal.into();
|
let mut bal: bal::ActiveModel = bal.into();
|
||||||
bal.state = Set(BalState::Ended);
|
bal.state = Set(BalState::Ended);
|
||||||
|
|
||||||
match bal.update(state.db_conn.as_ref()).await {
|
let model = bal.update(state.db_conn.as_ref()).await.unwrap().try_into_model().expect("All fields should be set once the bal is saved");
|
||||||
Err(e) => {
|
|
||||||
log::error!(target: "api", "Error while updating bal from api: {:#?}", e);
|
let self_owner = Owner::find()
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
|
.filter(owner::Column::User.eq(true))
|
||||||
},
|
.filter(owner::Column::UserId.eq(claims.user_id))
|
||||||
Ok(res) => {
|
.one(state.db_conn.as_ref()).await.unwrap().unwrap();
|
||||||
let model = res.try_into_model().expect("All fields should be set once the bal is saved");
|
|
||||||
(StatusCode::OK, Json(Some(model)))
|
let mut total_sold_books = 0;
|
||||||
|
let mut total_owned_sold_books = 0;
|
||||||
|
let mut total_collected_money = 0.;
|
||||||
|
let mut total_owned_collected_money = 0.;
|
||||||
|
|
||||||
|
let mut owner_set = HashSet::new();
|
||||||
|
for book in BookInstance::find()
|
||||||
|
.filter(book_instance::Column::BalId.eq(id))
|
||||||
|
.all(state.db_conn.as_ref()).await.unwrap() {
|
||||||
|
if book.sold_price.is_some() {
|
||||||
|
owner_set.insert(book.owner_id);
|
||||||
|
|
||||||
|
total_sold_books += 1;
|
||||||
|
total_collected_money += book.sold_price.unwrap();
|
||||||
|
if book.owner_id == self_owner.id {
|
||||||
|
total_owned_sold_books += 1;
|
||||||
|
total_owned_collected_money += book.sold_price.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bal_stats = bal_stats::ActiveModel {
|
||||||
|
id: NotSet,
|
||||||
|
bal_id: Set(id),
|
||||||
|
|
||||||
|
total_sold_books: Set(total_sold_books),
|
||||||
|
total_owned_sold_books: Set(total_owned_sold_books),
|
||||||
|
|
||||||
|
total_collected_money: Set(total_collected_money),
|
||||||
|
total_owned_collected_money: Set(total_owned_collected_money),
|
||||||
|
|
||||||
|
total_different_owners: Set(owner_set.len().try_into().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
bal_stats.save(state.db_conn.as_ref()).await.unwrap();
|
||||||
|
(StatusCode::OK, Json(Some(model)))
|
||||||
} else {
|
} else {
|
||||||
(StatusCode::NOT_FOUND, Json(None))
|
(StatusCode::NOT_FOUND, Json(None))
|
||||||
}
|
}
|
||||||
|
|
@ -349,3 +382,109 @@ pub async fn get_bal_accounting(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||||
|
pub struct BalAccountingReturnParams {
|
||||||
|
return_type: ReturnType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||||
|
pub enum ReturnType {
|
||||||
|
Books,
|
||||||
|
Money,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoParams)]
|
||||||
|
#[into_params(names("id", "owner_id"), parameter_in = Path)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct BalAndOwnerByIdParams(u32, u32);
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/bal/{id}/accounting/return/{owner_id}",
|
||||||
|
params(BalAndOwnerByIdParams),
|
||||||
|
request_body = BalAccountingReturnParams,
|
||||||
|
security(("jwt" = [])),
|
||||||
|
responses(
|
||||||
|
(status = OK, description = "Successfully returned requested type to owner"),
|
||||||
|
(status = CONFLICT, description = "Books and money have already been returned to the owner, or no money nor books were owed"),
|
||||||
|
(status = NOT_FOUND, description = "No bal or owner with this id exists in the database"),
|
||||||
|
(status = FORBIDDEN, description = "You don't own the specified bal or owner"),
|
||||||
|
),
|
||||||
|
summary = "Return books and/or money to an owner",
|
||||||
|
description = "Return books and/or money to an owner",
|
||||||
|
tag = "bal-api",
|
||||||
|
)]
|
||||||
|
pub async fn return_to_owner(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
claims: Claims,
|
||||||
|
Path((id, owner_id)): Path<(u32, u32)>,
|
||||||
|
Json(payload): Json<BalAccountingReturnParams>
|
||||||
|
) -> StatusCode {
|
||||||
|
if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await && let Ok(Some(owner)) = Owner::find_by_id(owner_id).one(state.db_conn.as_ref()).await {
|
||||||
|
if bal.user_id != claims.user_id || owner.user_id != claims.user_id {
|
||||||
|
StatusCode::FORBIDDEN
|
||||||
|
} else {
|
||||||
|
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() {
|
||||||
|
return StatusCode::CONFLICT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut book_delete_query = BookInstance::delete_many()
|
||||||
|
.filter(book_instance::Column::OwnerId.eq(owner.id))
|
||||||
|
.filter(book_instance::Column::BalId.eq(bal.id));
|
||||||
|
book_delete_query = match payload.return_type {
|
||||||
|
ReturnType::Books => book_delete_query.filter(book_instance::Column::SoldPrice.is_null()),
|
||||||
|
ReturnType::Money => book_delete_query.filter(book_instance::Column::SoldPrice.is_not_null()),
|
||||||
|
ReturnType::All => book_delete_query
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = book_delete_query.exec(state.db_conn.as_ref()).await;
|
||||||
|
StatusCode::OK
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StatusCode::NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[axum::debug_handler]
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/bal/{id}/stats",
|
||||||
|
params(BalByIdParams),
|
||||||
|
security(("jwt" = [])),
|
||||||
|
responses(
|
||||||
|
(status = OK, body = bal_stats::Model, description = "Found bal stats with corresponding ID in the database"),
|
||||||
|
(status = CONFLICT, description = "The specified BAL is not ended yet, can't get stats"),
|
||||||
|
(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's stats by its ID",
|
||||||
|
description = "Get a bal's stats from its ID",
|
||||||
|
tag = "bal-api",
|
||||||
|
)]
|
||||||
|
pub async fn get_stats_by_bal_id(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
claims: Claims,
|
||||||
|
Path(id): Path<u32>,
|
||||||
|
) -> (StatusCode, Json<Option<bal_stats::Model>>) {
|
||||||
|
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 if bal.state != BalState::Ended {
|
||||||
|
(StatusCode::CONFLICT, Json(None))
|
||||||
|
} else {
|
||||||
|
if let Ok(Some(stats)) = BalStats::find().filter(bal_stats::Column::BalId.eq(bal.id)).one(state.db_conn.as_ref()).await {
|
||||||
|
(StatusCode::OK, Json(Some(stats)))
|
||||||
|
} else {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(StatusCode::NOT_FOUND, Json(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Reference in a new issue