From 0291a9fb99f5df51f3a69658e9be8fdcf3b2585a Mon Sep 17 00:00:00 2001 From: Samuel Shuert Date: Mon, 29 Jun 2026 02:07:39 +0000 Subject: [PATCH] feat(backend): add customers --- backend/src/models/customer.rs | 77 ++++++++++++++ backend/src/routes/batch.rs | 7 +- backend/src/routes/customer.rs | 186 +++++++++++++++++++++++++++++++++ backend/src/routes/mod.rs | 9 ++ backend/src/routes/template.rs | 2 + 5 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 backend/src/routes/customer.rs diff --git a/backend/src/models/customer.rs b/backend/src/models/customer.rs index e69de29..f89e6c9 100644 --- a/backend/src/models/customer.rs +++ b/backend/src/models/customer.rs @@ -0,0 +1,77 @@ +use super::{ApplyOption, CrudBackend, Update}; +use crate::models::user::Permissions; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use sqlx::prelude::FromRow; +use uuid::Uuid; + +#[derive(Debug, Deserialize, Serialize, Clone, FromRow)] +pub struct Customer { + pub id: Uuid, + pub name: String, + pub code: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CustomerUpdate { + pub name: Option, + pub code: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CustomerCreate { + flavor: Uuid, + lot_number: String, +} + +impl Update for CustomerUpdate { + fn apply(self, other: &mut Customer) -> bool { + let Self { name, code } = self; + name.apply_to(&mut other.name) | code.apply_to(&mut other.code) + } +} + +impl CrudBackend for Customer { + type Identifier = Uuid; + + type Create = CustomerCreate; + type Update = CustomerUpdate; + + const TABLE_NAME: &'static str = "customers"; + const PERM_READ: Permissions = Permissions::CustomerRead; + const PERM_CREATE: Permissions = Permissions::CustomerCreate; + const PERM_UPDATE: Permissions = Permissions::CustomerUpdate; + const PERM_DELETE: Permissions = Permissions::CustomerDelete; + + fn id(&self) -> Self::Identifier { + self.id + } + + async fn create_inner<'a>( + conn: impl sqlx::prelude::Executor<'a, Database = sqlx::Postgres>, + create: Self::Create, + ) -> Result { + todo!() + } + + async fn read_inner<'a>( + conn: impl sqlx::prelude::Executor<'a, Database = sqlx::Postgres>, + id: Self::Identifier, + ) -> Result { + todo!() + } + + async fn update_inner<'a>( + &mut self, + conn: impl sqlx::prelude::Executor<'a, Database = sqlx::Postgres>, + ) -> Result<()> { + todo!() + } + + async fn delete_inner<'a>( + &self, + conn: impl sqlx::prelude::Executor<'a, Database = sqlx::Postgres>, + ) -> Result<()> { + todo!() + } +} diff --git a/backend/src/routes/batch.rs b/backend/src/routes/batch.rs index c58ddec..9018c40 100644 --- a/backend/src/routes/batch.rs +++ b/backend/src/routes/batch.rs @@ -1,3 +1,4 @@ +use super::SortOrder; use crate::models::{ Crud, CrudBackend, batch::{Batch, BatchCreate, BatchUpdate}, @@ -15,12 +16,6 @@ type Main = Batch; type Create = BatchCreate; type Update = BatchUpdate; -#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] -enum SortOrder { - Asc, - Desc, -} - #[derive(Debug, Clone, Deserialize)] struct QueryParams { pub limit: Option, diff --git a/backend/src/routes/customer.rs b/backend/src/routes/customer.rs new file mode 100644 index 0000000..ee1124c --- /dev/null +++ b/backend/src/routes/customer.rs @@ -0,0 +1,186 @@ +use super::SortOrder; +use crate::models::{ + Crud, CrudBackend, + customer::{Customer, CustomerCreate, CustomerUpdate}, + user::User, +}; +use actix_web::{ + HttpResponse, Responder, Scope, + web::{Data, Json, Path, Query}, +}; +use serde::Deserialize; +use sqlx::PgPool; +use uuid::Uuid; + +type Main = Customer; +type Create = CustomerCreate; +type Update = CustomerUpdate; + +#[derive(Debug, Clone, Deserialize)] +struct QueryParams { + pub limit: Option, + pub offset: Option, + pub sort_order: Option, +} + +#[actix_web::post("")] +async fn post(pool: Data, body: Json, user: User) -> impl Responder { + if !user.permissions.contains(Main::PERM_CREATE) { + return HttpResponse::Forbidden().body(format!( + "You are not authorized to create {}", + Main::TABLE_NAME + )); + } + match Main::create(pool.get_ref(), body.into_inner()).await { + Ok(batch) => HttpResponse::Created().json(batch), + Err(err) => { + tracing::error!("{err:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +#[actix_web::get("")] +async fn get_all(pool: Data, options: Query, user: User) -> impl Responder { + if !user.permissions.contains(Main::PERM_READ) { + return HttpResponse::Forbidden().body(format!( + "You are not authorized to get {}", + Main::TABLE_NAME + )); + } + + let limit = options.limit.unwrap_or(100) as i64; + let offset = options.offset.unwrap_or(0) as i64; + let sort_asc = options.sort_order.unwrap_or(SortOrder::Desc) == SortOrder::Asc; + + let mut query = sqlx::QueryBuilder::new(format!("SELECT * FROM {}", Main::TABLE_NAME)); + query.push(" WHERE 1=1"); + + // TODO: ADD more filters + + query.push(" ORDER BY name "); + if sort_asc { + query.push("ASC"); + } else { + query.push("DESC"); + } + + query.push(" LIMIT "); + query.push_bind(limit); + query.push(" OFFSET "); + query.push_bind(offset); + + match query + .build_query_as::
() + .fetch_all(pool.get_ref()) + .await + { + Ok(batches) => HttpResponse::Ok().json(batches), + Err(err) => { + tracing::error!("{err:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +#[actix_web::get("/{id}")] +async fn get(pool: Data, id: Path, user: User) -> impl Responder { + if !user.permissions.contains(Main::PERM_READ) { + return HttpResponse::Forbidden().body(format!( + "You are not authorized to get {}", + Main::TABLE_NAME + )); + } + + match Main::read(pool.get_ref(), id.into_inner()).await { + Ok(resource) => HttpResponse::Ok().json(resource), + Err(err) => { + if let Some(sqlx_err) = err.downcast_ref::() { + match sqlx_err { + sqlx::Error::RowNotFound => return HttpResponse::NotFound().finish(), + _ => (), + } + } + tracing::error!("{err:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +#[actix_web::patch("/{id}")] +async fn patch( + pool: Data, + id: Path, + body: Json, + user: User, +) -> impl Responder { + if !user.permissions.contains(Main::PERM_UPDATE) { + return HttpResponse::Forbidden().body(format!( + "You are not authorized to update {}", + Main::TABLE_NAME + )); + } + + let mut resource = match Main::read(pool.get_ref(), id.into_inner()).await { + Ok(resource) => resource, + Err(err) => { + if let Some(sqlx_err) = err.downcast_ref::() { + match sqlx_err { + sqlx::Error::RowNotFound => return HttpResponse::NotFound().finish(), + _ => (), + } + } + tracing::error!("{err:?}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + match resource.update(pool.get_ref(), body.into_inner()).await { + Ok(()) => HttpResponse::Ok().json(resource), + Err(err) => { + tracing::error!("{err:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +#[actix_web::delete("/{id}")] +async fn delete(pool: Data, id: Path, user: User) -> impl Responder { + if !user.permissions.contains(Main::PERM_DELETE) { + return HttpResponse::Forbidden().body(format!( + "You are not authorized to delete {}", + Main::TABLE_NAME + )); + } + + let resource = match Main::read(pool.get_ref(), id.into_inner()).await { + Ok(resource) => resource, + Err(err) => { + if let Some(sqlx_err) = err.downcast_ref::() { + match sqlx_err { + sqlx::Error::RowNotFound => return HttpResponse::NotFound().finish(), + _ => (), + } + } + tracing::error!("{err:?}"); + return HttpResponse::InternalServerError().finish(); + } + }; + + match resource.delete(pool.get_ref()).await { + Ok(()) => HttpResponse::NoContent().finish(), + Err(err) => { + tracing::error!("{err:?}"); + HttpResponse::InternalServerError().finish() + } + } +} + +pub fn service() -> Scope { + actix_web::web::scope(&format!("/{}", Main::TABLE_NAME)) + .service(post) + .service(get) + .service(get_all) + .service(patch) + .service(delete) +} diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 8fab0f2..590b341 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,4 +1,13 @@ +use serde::Deserialize; + pub mod auth; pub mod batch; +pub mod customer; pub mod system; pub mod user; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] +pub enum SortOrder { + Asc, + Desc, +} diff --git a/backend/src/routes/template.rs b/backend/src/routes/template.rs index abc7798..3929390 100644 --- a/backend/src/routes/template.rs +++ b/backend/src/routes/template.rs @@ -1,9 +1,11 @@ +use super::SortOrder; use crate::models::{Crud, CrudBackend, user::User}; use actix_web::{ HttpResponse, Responder, Scope, web::{Data, Json, Path, Query}, }; +use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; -- 2.51.2