351 lines
12 KiB
Rust
351 lines
12 KiB
Rust
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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
) -> (StatusCode, Json<Option<bal::Model>>) {
|
|
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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Json(instance_payload): Json<BalCreateParams>,
|
|
) -> (StatusCode, Json<Option<bal::Model>>) {
|
|
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<String>,
|
|
start_timestamp: Option<i64>,
|
|
end_timestamp: Option<i64>,
|
|
}
|
|
|
|
#[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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
Json(payload): Json<BalUpdateParams>,
|
|
) -> (StatusCode, Json<Option<bal::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 {
|
|
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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
) -> (StatusCode, Json<Option<bal::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 {
|
|
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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>,
|
|
) -> (StatusCode, Json<Option<bal::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 {
|
|
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<bal::Model>, 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<Arc<AppState>>,
|
|
claims: Claims,
|
|
) -> (StatusCode, Json<Vec<bal::Model>>) {
|
|
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<book_instance::Model>
|
|
}
|
|
|
|
#[derive(Serialize, utoipa::ToSchema)]
|
|
pub struct BalAccounting {
|
|
pub owner_map: HashMap<u32, OwnerAccountingData>,
|
|
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<Arc<AppState>>,
|
|
claims: Claims,
|
|
Path(id): Path<u32>
|
|
) -> (StatusCode, Json<Option<BalAccounting>>) {
|
|
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))
|
|
}
|
|
}
|
|
|