// rocie - An enterprise grocery management system // // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use actix_identity::Identity; use actix_web::{HttpRequest, HttpResponse, Responder, Result, get, web}; use crate::{ api::get::auth::UrlEncodedString, app::App, storage::sql::{ product::{Product, ProductId, ProductIdStub}, product_amount::ProductAmount, product_parent::{ProductParent, ProductParentId, ProductParentIdStub}, }, }; /// Get Product by id #[utoipa::path( responses( ( status = OK, description = "Product found from database", body = Product ), ( status = NOT_FOUND, description = "Product not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = ProductId, description = "Product id" ), ) )] #[get("/product/by-id/{id}")] pub(crate) async fn product_by_id( app: web::Data, id: web::Path, _user: Identity, ) -> Result { let id = id.into_inner(); match Product::from_id(&app, id.into()).await? { Some(product) => Ok(HttpResponse::Ok().json(product)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get Product by name #[utoipa::path( responses( ( status = OK, description = "Product found from database", body = Product ), ( status = NOT_FOUND, description = "Product not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "name" = String, description = "Name of the product" ), ) )] #[get("/product/by-name/{name}")] pub(crate) async fn product_by_name( app: web::Data, req: HttpRequest, name: web::Path, _user: Identity, ) -> Result { drop(name); let name = UrlEncodedString::from_str( req.path() .strip_prefix("/product/by-name/") .expect("Will always exists"), ); let name = name.percent_decode()?; match Product::from_name(&app, name.as_str()).await? { Some(product) => Ok(HttpResponse::Ok().json(product)), None => Ok(HttpResponse::NotFound().finish()), } } /// Get Product suggestion by name #[utoipa::path( responses( ( status = OK, description = "Product suggestions found from database", body = Vec ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "name" = String, description = "Partial name of a product" ), ) )] #[get("/product/by-part-name/{name}")] pub(crate) async fn product_suggestion_by_name( app: web::Data, req: HttpRequest, name: web::Path, _user: Identity, ) -> Result { drop(name); let name = UrlEncodedString::from_str( req.path() .strip_prefix("/product/by-part-name/") .expect("Will always exists"), ); let name = &name.percent_decode()?; let all = Product::get_all(&app).await?; let matching = all .into_iter() .filter(|product| product.name.starts_with(name.as_str())) .collect::>(); Ok(HttpResponse::Ok().json(matching)) } /// Return all registered products #[utoipa::path( responses( ( status = OK, description = "All products found", body = Vec ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[get("/products_registered/")] pub(crate) async fn products_registered( app: web::Data, _user: Identity, ) -> Result { let all = Product::get_all(&app).await?; Ok(HttpResponse::Ok().json(all)) } /// Return all products, which non-null amount in storage #[utoipa::path( responses( ( status = OK, description = "All products found", body = Vec ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[get("/products_in_storage/")] pub(crate) async fn products_in_storage( app: web::Data, _user: Identity, ) -> Result { let all = Product::get_all(&app).await?; let mut output_products = Vec::with_capacity(all.len()); for product in all { let amount = ProductAmount::from_id(&app, product.id).await?; if amount.is_some_and(|amount| amount.amount.value > 0) { output_products.push(product); } } Ok(HttpResponse::Ok().json(output_products)) } /// Get Products by it's product parent id /// /// This will also return all products below this product parent id #[utoipa::path( responses( ( status = OK, description = "Products found from database", body = Vec ), ( status = NOT_FOUND, description = "Product parent id not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = ProductParentId, description = "Product parent id" ), ) )] #[get("/product/by-product-parent-id-indirect/{id}")] pub(crate) async fn products_by_product_parent_id_indirect( app: web::Data, id: web::Path, _user: Identity, ) -> Result { let id = id.into_inner(); if let Some(parent) = ProductParent::from_id(&app, id.into()).await? { async fn collect_products(app: &App, parent: ProductParent) -> Result> { let mut all = Product::get_all(app) .await? .into_iter() .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) .collect::>(); if let Some(child) = ProductParent::get_all(app) .await? .into_iter() .find(|pp| pp.parent.is_some_and(|id| id == parent.id)) { all.extend(Box::pin(collect_products(app, child)).await?); } Ok(all) } let all = collect_products(&app, parent).await?; Ok(HttpResponse::Ok().json(all)) } else { Ok(HttpResponse::NotFound().finish()) } } /// Get Products by it's product parent id /// /// This will only return products directly associated with this product parent id #[utoipa::path( responses( ( status = OK, description = "Products found from database", body = Vec ), ( status = NOT_FOUND, description = "Product parent id not found in database" ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), params( ( "id" = ProductParentId, description = "Product parent id" ), ) )] #[get("/product/by-product-parent-id-direct/{id}")] pub(crate) async fn products_by_product_parent_id_direct( app: web::Data, id: web::Path, _user: Identity, ) -> Result { let id = id.into_inner(); if let Some(parent) = ProductParent::from_id(&app, id.into()).await? { let all = Product::get_all(&app) .await? .into_iter() .filter(|prod| prod.parent.is_some_and(|val| val == parent.id)) .collect::>(); Ok(HttpResponse::Ok().json(all)) } else { Ok(HttpResponse::NotFound().finish()) } } /// Get Products by it's absents of a product parent /// /// This will only return products without a product parent associated with it #[utoipa::path( responses( ( status = OK, description = "Products found from database", body = Vec ), ( status = UNAUTHORIZED, description = "You did not login before calling this endpoint", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[get("/product/without-product-parent")] pub(crate) async fn products_without_product_parent( app: web::Data, _user: Identity, ) -> Result { let all = { let base = Product::get_all(&app).await?; base.into_iter() .filter(|r| r.parent.is_none()) .collect::>() }; Ok(HttpResponse::Ok().json(all)) }