feat: book return and bal stats
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Ninjdai 2025-08-15 13:14:11 +02:00
parent 910b10de35
commit b817d36816
6 changed files with 193 additions and 10 deletions

View file

@ -34,6 +34,8 @@ where C: ConnectionTrait {
pub enum Relation {
#[sea_orm(has_many = "super::book_instance::Entity")]
BookInstance,
#[sea_orm(has_one = "super::bal_stats::Entity")]
BalStats,
}
impl Related<super::book_instance::Entity> for Entity {

38
src/entities/bal_stats.rs Normal file
View 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 {}

View file

@ -1,6 +1,7 @@
pub mod prelude;
pub mod bal;
pub mod bal_stats;
pub mod book;
pub mod book_instance;
pub mod owner;

View file

@ -1,5 +1,6 @@
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_instance::Entity as BookInstance;
pub use super::owner::Entity as Owner;

View file

@ -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::get_bals))
.routes(routes!(routes::bal::get_bal_accounting))
.routes(routes!(routes::bal::return_to_owner))
.routes(routes!(routes::bal::get_stats_by_bal_id))
// Authentication
.route_layer(middleware::from_fn_with_state(shared_state.clone(), routes::auth::auth_middleware))
.routes(routes!(routes::auth::auth))

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::Arc};
use std::{collections::{HashMap, HashSet}, sync::Arc};
use axum::{extract::{Path, State}, Json};
use reqwest::{StatusCode};
@ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityT
use serde::{Deserialize, Serialize};
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)]
@ -242,16 +242,49 @@ pub async fn stop_bal(
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)))
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");
let self_owner = Owner::find()
.filter(owner::Column::User.eq(true))
.filter(owner::Column::UserId.eq(claims.user_id))
.one(state.db_conn.as_ref()).await.unwrap().unwrap();
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 {
(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))
}
}