use std::str::FromStr; use axum::{ extract::{Path, Query, State}, http::StatusCode, routing::{get, put}, Json, Router, }; use bigdecimal::BigDecimal; use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; use crate::core::bot::get_default_bot; use crate::core::shared::schema::{ inventory_movements, price_list_items, price_lists, product_categories, product_variants, products, services, }; use crate::core::shared::state::AppState; #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = products)] pub struct Product { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub sku: Option, pub name: String, pub description: Option, pub category: Option, pub product_type: String, pub price: BigDecimal, pub cost: Option, pub currency: String, pub tax_rate: BigDecimal, pub unit: String, pub stock_quantity: i32, pub low_stock_threshold: i32, pub is_active: bool, pub images: serde_json::Value, pub attributes: serde_json::Value, pub weight: Option, pub dimensions: Option, pub barcode: Option, pub created_at: DateTime, pub updated_at: DateTime, // // Tax and fiscal identification // pub tax_code: Option, // pub tax_class: Option, // pub fiscal_code: Option, // pub origin_code: Option, // pub global_trade_number: Option, // pub tax_unit_code: Option, // // Detailed dimensions (shipping) // pub net_weight: Option, // pub gross_weight: Option, // pub width: Option, // pub height: Option, // pub length: Option, // pub package_count: Option, // // Tax rates by type // pub sales_tax_code: Option, // pub sales_tax_rate: Option, // pub excise_tax_code: Option, // pub excise_tax_rate: Option, // pub vat_code: Option, // pub vat_rate: Option, // pub service_tax_code: Option, // pub service_tax_rate: Option, // // Marketplace and e-commerce // pub brand: Option, // pub model: Option, // pub color: Option, // pub size: Option, // pub material: Option, // pub gender: Option, // // Advanced inventory control // pub warehouse_location: Option, // pub batch_number: Option, // pub expiration_date: Option, // pub manufacture_date: Option, // pub min_stock: Option, // pub max_stock: Option, // pub reorder_point: Option, // // Detailed pricing // pub sale_price: Option, // pub sale_start: Option>, // pub sale_end: Option>, // pub shipping_cost: Option, // pub profit_margin: Option, // Payment gateway integration // pub external_id: Option, // pub external_category_id: Option, // pub external_metadata: Option, // // SEO and search // pub slug: Option, // pub meta_title: Option, // pub meta_description: Option, // pub tags: Option>>, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = services)] pub struct Service { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub name: String, pub description: Option, pub category: Option, pub service_type: String, pub hourly_rate: Option, pub fixed_price: Option, pub currency: String, pub duration_minutes: Option, pub is_active: bool, pub attributes: serde_json::Value, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = product_categories)] pub struct ProductCategory { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub name: String, pub description: Option, pub parent_id: Option, pub slug: Option, pub image_url: Option, pub sort_order: i32, pub is_active: bool, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable, AsChangeset)] #[diesel(table_name = price_lists)] pub struct PriceList { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub name: String, pub description: Option, pub currency: String, pub is_default: bool, pub valid_from: Option, pub valid_until: Option, pub customer_group: Option, pub discount_percent: BigDecimal, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = price_list_items)] pub struct PriceListItem { pub id: Uuid, pub price_list_id: Uuid, pub product_id: Option, pub service_id: Option, pub price: BigDecimal, pub min_quantity: i32, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = inventory_movements)] pub struct InventoryMovement { pub id: Uuid, pub org_id: Uuid, pub bot_id: Uuid, pub product_id: Uuid, pub movement_type: String, pub quantity: i32, pub reference_type: Option, pub reference_id: Option, pub notes: Option, pub created_by: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] #[diesel(table_name = product_variants)] pub struct ProductVariant { pub id: Uuid, pub product_id: Uuid, pub sku: Option, pub name: String, pub price_adjustment: BigDecimal, pub stock_quantity: i32, pub attributes: serde_json::Value, pub is_active: bool, pub created_at: DateTime, // Extended variant fields pub global_trade_number: Option, pub net_weight: Option, pub gross_weight: Option, pub width: Option, pub height: Option, pub length: Option, pub color: Option, pub size: Option, pub images: Option, } #[derive(Debug, Deserialize)] pub struct CreateProductRequest { pub name: String, pub description: Option, pub sku: Option, pub category: Option, pub product_type: Option, pub price: f64, pub cost: Option, pub currency: Option, pub tax_rate: Option, pub unit: Option, pub stock_quantity: Option, pub low_stock_threshold: Option, pub images: Option>, pub barcode: Option, pub weight: Option, } #[derive(Debug, Deserialize)] pub struct UpdateProductRequest { pub name: Option, pub description: Option, pub sku: Option, pub category: Option, pub price: Option, pub cost: Option, pub tax_rate: Option, pub unit: Option, pub stock_quantity: Option, pub low_stock_threshold: Option, pub is_active: Option, pub barcode: Option, } #[derive(Debug, Deserialize)] pub struct CreateServiceRequest { pub name: String, pub description: Option, pub category: Option, pub service_type: Option, pub hourly_rate: Option, pub fixed_price: Option, pub currency: Option, pub duration_minutes: Option, } #[derive(Debug, Deserialize)] pub struct UpdateServiceRequest { pub name: Option, pub description: Option, pub category: Option, pub hourly_rate: Option, pub fixed_price: Option, pub duration_minutes: Option, pub is_active: Option, } #[derive(Debug, Deserialize)] pub struct CreateCategoryRequest { pub name: String, pub description: Option, pub parent_id: Option, pub slug: Option, pub image_url: Option, } #[derive(Debug, Deserialize)] pub struct CreatePriceListRequest { pub name: String, pub description: Option, pub currency: Option, pub discount_percent: Option, pub customer_group: Option, pub valid_from: Option, pub valid_until: Option, } #[derive(Debug, Deserialize)] pub struct AdjustStockRequest { pub quantity: i32, pub movement_type: String, pub notes: Option, pub reference_type: Option, pub reference_id: Option, } #[derive(Debug, Deserialize)] pub struct ListQuery { pub search: Option, pub category: Option, pub is_active: Option, pub low_stock: Option, pub limit: Option, pub offset: Option, } #[derive(Debug, Serialize)] pub struct ProductStats { pub total_products: i64, pub active_products: i64, pub total_services: i64, pub active_services: i64, pub low_stock_count: i64, pub total_stock_value: f64, pub categories_count: i64, pub price_lists_count: i64, } #[derive(Debug, Serialize)] pub struct ProductWithVariants { pub product: Product, pub variants: Vec, } fn get_bot_context(state: &AppState) -> (Uuid, Uuid) { let Ok(mut conn) = state.conn.get() else { return (Uuid::nil(), Uuid::nil()); }; let (bot_id, _bot_name) = get_default_bot(&mut conn); let org_id = Uuid::nil(); (org_id, bot_id) } fn bd(val: f64) -> BigDecimal { BigDecimal::from_str(&val.to_string()).unwrap_or_else(|_| BigDecimal::from(0)) } fn bd_to_f64(val: &BigDecimal) -> f64 { val.to_string().parse::().unwrap_or(0.0) } pub async fn create_product( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let id = Uuid::new_v4(); let now = Utc::now(); let product = Product { id, org_id, bot_id, sku: req.sku, name: req.name, description: req.description, category: req.category, product_type: req.product_type.unwrap_or_else(|| "physical".to_string()), price: bd(req.price), cost: req.cost.map(bd), currency: req.currency.unwrap_or_else(|| "USD".to_string()), tax_rate: bd(req.tax_rate.unwrap_or(0.0)), unit: req.unit.unwrap_or_else(|| "unit".to_string()), stock_quantity: req.stock_quantity.unwrap_or(0), low_stock_threshold: req.low_stock_threshold.unwrap_or(10), is_active: true, images: serde_json::json!(req.images.unwrap_or_default()), attributes: serde_json::json!({}), weight: req.weight.map(bd), dimensions: None, barcode: req.barcode, created_at: now, updated_at: now, }; diesel::insert_into(products::table) .values(&product) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(product)) } pub async fn list_products( State(state): State>, Query(query): Query, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let limit = query.limit.unwrap_or(50); let offset = query.offset.unwrap_or(0); let mut q = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .into_boxed(); if let Some(is_active) = query.is_active { q = q.filter(products::is_active.eq(is_active)); } if let Some(category) = query.category { q = q.filter(products::category.eq(category)); } if let Some(true) = query.low_stock { q = q.filter(products::stock_quantity.le(products::low_stock_threshold)); } if let Some(search) = query.search { let pattern = format!("%{search}%"); q = q.filter( products::name .ilike(pattern.clone()) .or(products::sku.ilike(pattern.clone())) .or(products::description.ilike(pattern)), ); } let prods: Vec = q .order(products::name.asc()) .limit(limit) .offset(offset) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(prods)) } pub async fn get_product( State(state): State>, Path(id): Path, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let product: Product = products::table .filter(products::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Product not found".to_string()))?; let variants: Vec = product_variants::table .filter(product_variants::product_id.eq(id)) .order(product_variants::name.asc()) .load(&mut conn) .unwrap_or_default(); Ok(Json(ProductWithVariants { product, variants })) } pub async fn update_product( State(state): State>, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let now = Utc::now(); diesel::update(products::table.filter(products::id.eq(id))) .set(products::updated_at.eq(now)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; if let Some(name) = req.name { diesel::update(products::table.filter(products::id.eq(id))) .set(products::name.eq(name)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(description) = req.description { diesel::update(products::table.filter(products::id.eq(id))) .set(products::description.eq(description)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(price) = req.price { diesel::update(products::table.filter(products::id.eq(id))) .set(products::price.eq(bd(price))) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(stock_quantity) = req.stock_quantity { diesel::update(products::table.filter(products::id.eq(id))) .set(products::stock_quantity.eq(stock_quantity)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(is_active) = req.is_active { diesel::update(products::table.filter(products::id.eq(id))) .set(products::is_active.eq(is_active)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(category) = req.category { diesel::update(products::table.filter(products::id.eq(id))) .set(products::category.eq(category)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } let product: Product = products::table .filter(products::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Product not found".to_string()))?; Ok(Json(product)) } pub async fn delete_product( State(state): State>, Path(id): Path, ) -> Result { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; diesel::delete(products::table.filter(products::id.eq(id))) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?; Ok(StatusCode::NO_CONTENT) } pub async fn adjust_stock( State(state): State>, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let now = Utc::now(); let product: Product = products::table .filter(products::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Product not found".to_string()))?; let new_quantity = match req.movement_type.as_str() { "in" | "purchase" | "return" | "adjustment_add" => product.stock_quantity + req.quantity, "out" | "sale" | "adjustment_remove" | "damage" => product.stock_quantity - req.quantity, "set" => req.quantity, _ => product.stock_quantity + req.quantity, }; diesel::update(products::table.filter(products::id.eq(id))) .set(( products::stock_quantity.eq(new_quantity), products::updated_at.eq(now), )) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; let movement = InventoryMovement { id: Uuid::new_v4(), org_id, bot_id, product_id: id, movement_type: req.movement_type, quantity: req.quantity, reference_type: req.reference_type, reference_id: req.reference_id, notes: req.notes, created_by: None, created_at: now, }; diesel::insert_into(inventory_movements::table) .values(&movement) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; let updated: Product = products::table .filter(products::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Product not found".to_string()))?; Ok(Json(updated)) } pub async fn create_service( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let id = Uuid::new_v4(); let now = Utc::now(); let service = Service { id, org_id, bot_id, name: req.name, description: req.description, category: req.category, service_type: req.service_type.unwrap_or_else(|| "hourly".to_string()), hourly_rate: req.hourly_rate.map(bd), fixed_price: req.fixed_price.map(bd), currency: req.currency.unwrap_or_else(|| "USD".to_string()), duration_minutes: req.duration_minutes, is_active: true, attributes: serde_json::json!({}), created_at: now, updated_at: now, }; diesel::insert_into(services::table) .values(&service) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(service)) } pub async fn list_services( State(state): State>, Query(query): Query, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let limit = query.limit.unwrap_or(50); let offset = query.offset.unwrap_or(0); let mut q = services::table .filter(services::org_id.eq(org_id)) .filter(services::bot_id.eq(bot_id)) .into_boxed(); if let Some(is_active) = query.is_active { q = q.filter(services::is_active.eq(is_active)); } if let Some(category) = query.category { q = q.filter(services::category.eq(category)); } if let Some(search) = query.search { let pattern = format!("%{search}%"); q = q.filter( services::name .ilike(pattern.clone()) .or(services::description.ilike(pattern)), ); } let svcs: Vec = q .order(services::name.asc()) .limit(limit) .offset(offset) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(svcs)) } pub async fn get_service( State(state): State>, Path(id): Path, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let service: Service = services::table .filter(services::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Service not found".to_string()))?; Ok(Json(service)) } pub async fn update_service( State(state): State>, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let now = Utc::now(); diesel::update(services::table.filter(services::id.eq(id))) .set(services::updated_at.eq(now)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; if let Some(name) = req.name { diesel::update(services::table.filter(services::id.eq(id))) .set(services::name.eq(name)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(description) = req.description { diesel::update(services::table.filter(services::id.eq(id))) .set(services::description.eq(description)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(hourly_rate) = req.hourly_rate { diesel::update(services::table.filter(services::id.eq(id))) .set(services::hourly_rate.eq(Some(bd(hourly_rate)))) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } if let Some(is_active) = req.is_active { diesel::update(services::table.filter(services::id.eq(id))) .set(services::is_active.eq(is_active)) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Update error: {e}")))?; } let service: Service = services::table .filter(services::id.eq(id)) .first(&mut conn) .map_err(|_| (StatusCode::NOT_FOUND, "Service not found".to_string()))?; Ok(Json(service)) } pub async fn delete_service( State(state): State>, Path(id): Path, ) -> Result { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; diesel::delete(services::table.filter(services::id.eq(id))) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Delete error: {e}")))?; Ok(StatusCode::NO_CONTENT) } pub async fn list_categories( State(state): State>, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let cats: Vec = product_categories::table .filter(product_categories::org_id.eq(org_id)) .filter(product_categories::bot_id.eq(bot_id)) .filter(product_categories::is_active.eq(true)) .order(product_categories::sort_order.asc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(cats)) } pub async fn create_category( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let id = Uuid::new_v4(); let now = Utc::now(); let max_order: Option = product_categories::table .filter(product_categories::org_id.eq(org_id)) .filter(product_categories::bot_id.eq(bot_id)) .select(diesel::dsl::max(product_categories::sort_order)) .first(&mut conn) .unwrap_or(None); let category = ProductCategory { id, org_id, bot_id, name: req.name, description: req.description, parent_id: req.parent_id, slug: req.slug, image_url: req.image_url, sort_order: max_order.unwrap_or(0) + 1, is_active: true, created_at: now, }; diesel::insert_into(product_categories::table) .values(&category) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(category)) } pub async fn list_price_lists( State(state): State>, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let lists: Vec = price_lists::table .filter(price_lists::org_id.eq(org_id)) .filter(price_lists::bot_id.eq(bot_id)) .filter(price_lists::is_active.eq(true)) .order(price_lists::name.asc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(lists)) } pub async fn create_price_list( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let id = Uuid::new_v4(); let now = Utc::now(); let valid_from = req .valid_from .and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok()); let valid_until = req .valid_until .and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok()); let price_list = PriceList { id, org_id, bot_id, name: req.name, description: req.description, currency: req.currency.unwrap_or_else(|| "USD".to_string()), is_default: false, valid_from, valid_until, customer_group: req.customer_group, discount_percent: bd(req.discount_percent.unwrap_or(0.0)), is_active: true, created_at: now, updated_at: now, }; diesel::insert_into(price_lists::table) .values(&price_list) .execute(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert error: {e}")))?; Ok(Json(price_list)) } pub async fn list_inventory_movements( State(state): State>, Path(product_id): Path, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let movements: Vec = inventory_movements::table .filter(inventory_movements::product_id.eq(product_id)) .order(inventory_movements::created_at.desc()) .limit(100) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(movements)) } pub async fn get_product_stats( State(state): State>, ) -> Result, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let total_products: i64 = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .count() .get_result(&mut conn) .unwrap_or(0); let active_products: i64 = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .filter(products::is_active.eq(true)) .count() .get_result(&mut conn) .unwrap_or(0); let total_services: i64 = services::table .filter(services::org_id.eq(org_id)) .filter(services::bot_id.eq(bot_id)) .count() .get_result(&mut conn) .unwrap_or(0); let active_services: i64 = services::table .filter(services::org_id.eq(org_id)) .filter(services::bot_id.eq(bot_id)) .filter(services::is_active.eq(true)) .count() .get_result(&mut conn) .unwrap_or(0); let low_stock_count: i64 = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .filter(products::is_active.eq(true)) .filter(products::stock_quantity.le(products::low_stock_threshold)) .count() .get_result(&mut conn) .unwrap_or(0); let categories_count: i64 = product_categories::table .filter(product_categories::org_id.eq(org_id)) .filter(product_categories::bot_id.eq(bot_id)) .filter(product_categories::is_active.eq(true)) .count() .get_result(&mut conn) .unwrap_or(0); let price_lists_count: i64 = price_lists::table .filter(price_lists::org_id.eq(org_id)) .filter(price_lists::bot_id.eq(bot_id)) .filter(price_lists::is_active.eq(true)) .count() .get_result(&mut conn) .unwrap_or(0); let all_products: Vec = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .filter(products::is_active.eq(true)) .load(&mut conn) .unwrap_or_default(); let total_stock_value: f64 = all_products .iter() .map(|p| bd_to_f64(&p.price) * p.stock_quantity as f64) .sum(); let stats = ProductStats { total_products, active_products, total_services, active_services, low_stock_count, total_stock_value, categories_count, price_lists_count, }; Ok(Json(stats)) } pub async fn list_low_stock( State(state): State>, ) -> Result>, (StatusCode, String)> { let mut conn = state.conn.get().map_err(|e| { (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")) })?; let (org_id, bot_id) = get_bot_context(&state); let prods: Vec = products::table .filter(products::org_id.eq(org_id)) .filter(products::bot_id.eq(bot_id)) .filter(products::is_active.eq(true)) .filter(products::stock_quantity.le(products::low_stock_threshold)) .order(products::stock_quantity.asc()) .load(&mut conn) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Query error: {e}")))?; Ok(Json(prods)) } pub fn configure_products_api_routes() -> Router> { Router::new() .route("/api/products/items", get(list_products).post(create_product)) .route("/api/products/items/:id", get(get_product).put(update_product).delete(delete_product)) .route("/api/products/items/:id/stock", put(adjust_stock)) .route("/api/products/items/:id/movements", get(list_inventory_movements)) .route("/api/products/services", get(list_services).post(create_service)) .route("/api/products/services/:id", get(get_service).put(update_service).delete(delete_service)) .route("/api/products/categories", get(list_categories).post(create_category)) .route("/api/products/price-lists", get(list_price_lists).post(create_price_list)) .route("/api/products/stats", get(get_product_stats)) .route("/api/products/low-stock", get(list_low_stock)) }