use chrono::{DateTime, Utc, Duration}; use serde::{Deserialize, Serialize}; use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE; /// ProductType represents the type of a product #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ProductType { Product, Service, } /// ProductStatus represents the status of a product #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ProductStatus { Active, Error, EndOfLife, Paused, Available, Unavailable, } /// ProductComponent represents a component of a product #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProductComponent { pub id: u32, pub name: String, pub description: String, pub quantity: i32, pub created_at: DateTime, pub updated_at: DateTime, pub energy_usage: f64, // Energy usage in watts pub cost: Currency, // Cost of the component } impl ProductComponent { /// Create a new product component with default timestamps pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self { let now = Utc::now(); Self { id, name, description, quantity, created_at: now, updated_at: now, energy_usage: 0.0, cost: Currency::new(0.0, "USD".to_string()), } } /// Get the total energy usage for this component (energy_usage * quantity) pub fn total_energy_usage(&self) -> f64 { self.energy_usage * self.quantity as f64 } /// Get the total cost for this component (cost * quantity) pub fn total_cost(&self) -> Currency { Currency::new(self.cost.amount * self.quantity as f64, self.cost.currency_code.clone()) } } /// Builder for ProductComponent pub struct ProductComponentBuilder { id: Option, name: Option, description: Option, quantity: Option, created_at: Option>, updated_at: Option>, energy_usage: Option, cost: Option, } impl ProductComponentBuilder { /// Create a new ProductComponentBuilder with all fields set to None pub fn new() -> Self { Self { id: None, name: None, description: None, quantity: None, created_at: None, updated_at: None, energy_usage: None, cost: None, } } /// Set the id pub fn id(mut self, id: u32) -> Self { self.id = Some(id); self } /// Set the name pub fn name>(mut self, name: S) -> Self { self.name = Some(name.into()); self } /// Set the description pub fn description>(mut self, description: S) -> Self { self.description = Some(description.into()); self } /// Set the quantity pub fn quantity(mut self, quantity: i32) -> Self { self.quantity = Some(quantity); self } /// Set the created_at timestamp pub fn created_at(mut self, created_at: DateTime) -> Self { self.created_at = Some(created_at); self } /// Set the updated_at timestamp pub fn updated_at(mut self, updated_at: DateTime) -> Self { self.updated_at = Some(updated_at); self } /// Set the energy usage in watts pub fn energy_usage(mut self, energy_usage: f64) -> Self { self.energy_usage = Some(energy_usage); self } /// Set the cost pub fn cost(mut self, cost: Currency) -> Self { self.cost = Some(cost); self } /// Build the ProductComponent object pub fn build(self) -> Result { let now = Utc::now(); Ok(ProductComponent { id: self.id.ok_or("id is required")?, name: self.name.ok_or("name is required")?, description: self.description.ok_or("description is required")?, quantity: self.quantity.ok_or("quantity is required")?, created_at: self.created_at.unwrap_or(now), updated_at: self.updated_at.unwrap_or(now), energy_usage: self.energy_usage.unwrap_or(0.0), cost: self.cost.unwrap_or_else(|| Currency::new(0.0, "USD".to_string())), }) } } /// Product represents a product or service offered by the Freezone #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Product { pub id: u32, pub name: String, pub description: String, pub price: Currency, pub type_: ProductType, pub category: String, pub status: ProductStatus, pub created_at: DateTime, pub updated_at: DateTime, pub max_amount: u16, // means allows us to define how many max of this there are pub purchase_till: DateTime, pub active_till: DateTime, // after this product no longer active if e.g. a service pub components: Vec, } // Removed old Model trait implementation impl Product { /// Create a new product with default timestamps pub fn new( id: u32, name: String, description: String, price: Currency, type_: ProductType, category: String, status: ProductStatus, max_amount: u16, validity_days: i64, // How many days the product is valid after purchase ) -> Self { let now = Utc::now(); // Default: purchasable for 1 year, active for specified validity days after purchase Self { id, name, description, price, type_, category, status, created_at: now, updated_at: now, max_amount, purchase_till: now + Duration::days(365), active_till: now + Duration::days(validity_days), components: Vec::new(), } } /// Add a component to this product pub fn add_component(&mut self, component: ProductComponent) { self.components.push(component); self.updated_at = Utc::now(); } /// Update the purchase availability timeframe pub fn set_purchase_period(&mut self, purchase_till: DateTime) { self.purchase_till = purchase_till; self.updated_at = Utc::now(); } /// Update the active timeframe pub fn set_active_period(&mut self, active_till: DateTime) { self.active_till = active_till; self.updated_at = Utc::now(); } /// Check if the product is available for purchase pub fn is_purchasable(&self) -> bool { (self.status == ProductStatus::Available || self.status == ProductStatus::Active) && Utc::now() <= self.purchase_till } /// Check if the product is still active (for services) pub fn is_active(&self) -> bool { Utc::now() <= self.active_till } /// Calculate the total cost in the specified currency pub fn cost_in_currency(&self, currency_code: &str) -> Option { // If the price is already in the requested currency, return it if self.price.currency_code == currency_code { return Some(self.price.clone()); } // Convert the price to the requested currency self.price.to_currency(currency_code) } /// Calculate the total cost in USD pub fn cost_in_usd(&self) -> Option { self.cost_in_currency("USD") } /// Calculate the total energy usage of the product (sum of all components) pub fn total_energy_usage(&self) -> f64 { self.components.iter().map(|c| c.total_energy_usage()).sum() } /// Calculate the total cost of all components pub fn components_cost(&self, currency_code: &str) -> Option { if self.components.is_empty() { return Some(Currency::new(0.0, currency_code.to_string())); } // Sum up the costs of all components, converting to the requested currency let mut total = 0.0; for component in &self.components { let component_cost = component.total_cost(); if let Some(converted_cost) = component_cost.to_currency(currency_code) { total += converted_cost.amount; } else { return None; // Conversion failed } } Some(Currency::new(total, currency_code.to_string())) } /// Calculate the total cost of all components in USD pub fn components_cost_in_usd(&self) -> Option { self.components_cost("USD") } } /// Builder for Product pub struct ProductBuilder { id: Option, name: Option, description: Option, price: Option, type_: Option, category: Option, status: Option, created_at: Option>, updated_at: Option>, max_amount: Option, purchase_till: Option>, active_till: Option>, components: Vec, validity_days: Option, } impl ProductBuilder { /// Create a new ProductBuilder with all fields set to None pub fn new() -> Self { Self { id: None, name: None, description: None, price: None, type_: None, category: None, status: None, created_at: None, updated_at: None, max_amount: None, purchase_till: None, active_till: None, components: Vec::new(), validity_days: None, } } /// Set the id pub fn id(mut self, id: u32) -> Self { self.id = Some(id); self } /// Set the name pub fn name>(mut self, name: S) -> Self { self.name = Some(name.into()); self } /// Set the description pub fn description>(mut self, description: S) -> Self { self.description = Some(description.into()); self } /// Set the price pub fn price(mut self, price: Currency) -> Self { self.price = Some(price); self } /// Set the product type pub fn type_(mut self, type_: ProductType) -> Self { self.type_ = Some(type_); self } /// Set the category pub fn category>(mut self, category: S) -> Self { self.category = Some(category.into()); self } /// Set the status pub fn status(mut self, status: ProductStatus) -> Self { self.status = Some(status); self } /// Set the max amount pub fn max_amount(mut self, max_amount: u16) -> Self { self.max_amount = Some(max_amount); self } /// Set the validity days pub fn validity_days(mut self, validity_days: i64) -> Self { self.validity_days = Some(validity_days); self } /// Set the purchase_till date directly pub fn purchase_till(mut self, purchase_till: DateTime) -> Self { self.purchase_till = Some(purchase_till); self } /// Set the active_till date directly pub fn active_till(mut self, active_till: DateTime) -> Self { self.active_till = Some(active_till); self } /// Add a component to the product pub fn add_component(mut self, component: ProductComponent) -> Self { self.components.push(component); self } /// Build the Product object pub fn build(self) -> Result { let now = Utc::now(); let created_at = self.created_at.unwrap_or(now); let updated_at = self.updated_at.unwrap_or(now); // Calculate purchase_till and active_till based on validity_days if not set directly let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365)); let active_till = if let Some(validity_days) = self.validity_days { self.active_till.unwrap_or(now + Duration::days(validity_days)) } else { self.active_till.ok_or("Either active_till or validity_days must be provided")? }; Ok(Product { id: self.id.ok_or("id is required")?, name: self.name.ok_or("name is required")?, description: self.description.ok_or("description is required")?, price: self.price.ok_or("price is required")?, type_: self.type_.ok_or("type_ is required")?, category: self.category.ok_or("category is required")?, status: self.status.ok_or("status is required")?, created_at, updated_at, max_amount: self.max_amount.ok_or("max_amount is required")?, purchase_till, active_till, components: self.components, }) } } // Implement Storable trait (provides default dump/load) impl Storable for Product {} // Implement SledModel trait impl SledModel for Product { fn get_id(&self) -> String { self.id.to_string() } fn db_prefix() -> &'static str { "product" } } // Import Currency from the currency module use crate::models::biz::Currency;