441 lines
13 KiB
Rust
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; |