db/herodb/src/models/biz/product.rs
2025-04-04 12:15:30 +02:00

441 lines
13 KiB
Rust

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<Utc>,
pub updated_at: DateTime<Utc>,
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<u32>,
name: Option<String>,
description: Option<String>,
quantity: Option<i32>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
energy_usage: Option<f64>,
cost: Option<Currency>,
}
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<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(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<Utc>) -> Self {
self.created_at = Some(created_at);
self
}
/// Set the updated_at timestamp
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> 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<ProductComponent, &'static str> {
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<Utc>,
pub updated_at: DateTime<Utc>,
pub max_amount: u16, // means allows us to define how many max of this there are
pub purchase_till: DateTime<Utc>,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
pub components: Vec<ProductComponent>,
}
// 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<Utc>) {
self.purchase_till = purchase_till;
self.updated_at = Utc::now();
}
/// Update the active timeframe
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
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<Currency> {
// 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<Currency> {
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<Currency> {
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<Currency> {
self.components_cost("USD")
}
}
/// Builder for Product
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<Currency>,
type_: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
max_amount: Option<u16>,
purchase_till: Option<DateTime<Utc>>,
active_till: Option<DateTime<Utc>>,
components: Vec<ProductComponent>,
validity_days: Option<i64>,
}
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<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(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<S: Into<String>>(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<Utc>) -> Self {
self.purchase_till = Some(purchase_till);
self
}
/// Set the active_till date directly
pub fn active_till(mut self, active_till: DateTime<Utc>) -> 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<Product, &'static str> {
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;