diff --git a/herodb/Cargo.lock b/herodb/Cargo.lock index d45ba75..18eb964 100644 --- a/herodb/Cargo.lock +++ b/herodb/Cargo.lock @@ -658,6 +658,7 @@ dependencies = [ "bincode", "brotli", "chrono", + "lazy_static", "paste", "poem", "poem-openapi", @@ -831,6 +832,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.171" diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index 2dd1c57..143e48e 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -21,6 +21,7 @@ poem-openapi = { version = "2.0.11", features = ["swagger-ui"] } tokio = { version = "1", features = ["full"] } rhai = "1.15.1" paste = "1.0" +lazy_static = "1.4.0" [[example]] name = "rhai_demo" diff --git a/herodb/src/cmd/dbexample2/main.rs b/herodb/src/cmd/dbexample2/main.rs index d7e7215..162576a 100644 --- a/herodb/src/cmd/dbexample2/main.rs +++ b/herodb/src/cmd/dbexample2/main.rs @@ -1,10 +1,11 @@ -use chrono::{DateTime, Utc, Duration}; -use herodb::db::{DB, DBBuilder}; +use chrono::{Utc, Duration}; +use herodb::db::DBBuilder; use herodb::models::biz::{ Currency, CurrencyBuilder, - Product, ProductBuilder, ProductComponent, ProductComponentBuilder, + Product, ProductBuilder, ProductComponentBuilder, ProductType, ProductStatus, - Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus + Sale, SaleBuilder, SaleItemBuilder, SaleStatus, + ExchangeRate, ExchangeRateBuilder, EXCHANGE_RATE_SERVICE }; use std::path::PathBuf; use std::fs; @@ -26,6 +27,7 @@ fn main() -> Result<(), Box> { .register_model::() .register_model::() .register_model::() + .register_model::() .build()?; println!("\n1. Creating Products with Builder Pattern"); @@ -39,14 +41,19 @@ fn main() -> Result<(), Box> { // Insert the currency db.insert_currency(&usd)?; - println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code); + println!("Currency created: ${} {}", usd.amount, usd.currency_code); - // Create product components using the builder + // Create product components using the builder with energy usage and cost let component1 = ProductComponentBuilder::new() .id(101) .name("Basic Support") .description("24/7 email support") .quantity(1) + .energy_usage(5.0) // 5 watts + .cost(CurrencyBuilder::new() + .amount(5.0) + .currency_code("USD") + .build()?) .build()?; let component2 = ProductComponentBuilder::new() @@ -54,6 +61,11 @@ fn main() -> Result<(), Box> { .name("Premium Support") .description("24/7 phone and email support") .quantity(1) + .energy_usage(10.0) // 10 watts + .cost(CurrencyBuilder::new() + .amount(15.0) + .currency_code("USD") + .build()?) .build()?; // Create products using the builder @@ -67,7 +79,7 @@ fn main() -> Result<(), Box> { .build()?) .type_(ProductType::Service) .category("Subscription") - .status(ProductStatus::Available) + .status(ProductStatus::Active) .max_amount(1000) .validity_days(30) .add_component(component1) @@ -83,7 +95,7 @@ fn main() -> Result<(), Box> { .build()?) .type_(ProductType::Service) .category("Subscription") - .status(ProductStatus::Available) + .status(ProductStatus::Active) .max_amount(500) .validity_days(30) .add_component(component2) @@ -93,18 +105,32 @@ fn main() -> Result<(), Box> { db.insert_product(&product1)?; db.insert_product(&product2)?; - println!("Product created: {} (${:.2})", product1.name, product1.price.amount); - println!("Product created: {} (${:.2})", product2.name, product2.price.amount); + println!("Product created: {} (${}) USD", product1.name, product1.price.amount); + println!("Product created: {} (${}) USD", product2.name, product2.price.amount); println!("\n2. Retrieving Products"); println!("--------------------"); // Retrieve products using model-specific methods let retrieved_product1 = db.get_product(1)?; - println!("Retrieved: {} (${:.2})", retrieved_product1.name, retrieved_product1.price.amount); + println!("Retrieved: {} (${}) USD", retrieved_product1.name, retrieved_product1.price.amount); println!("Components:"); for component in &retrieved_product1.components { - println!(" - {} ({})", component.name, component.description); + println!(" - {} ({}, Energy: {}W, Cost: ${} USD)", + component.name, + component.description, + component.energy_usage, + component.cost.amount + ); + } + + // Calculate total energy usage + let total_energy = retrieved_product1.total_energy_usage(); + println!("Total energy usage: {}W", total_energy); + + // Calculate components cost + if let Some(components_cost) = retrieved_product1.components_cost_in_usd() { + println!("Total components cost: ${} USD", components_cost.amount); } println!("\n3. Listing All Products"); @@ -114,7 +140,7 @@ fn main() -> Result<(), Box> { let all_products = db.list_products()?; println!("Found {} products:", all_products.len()); for product in all_products { - println!(" - {} (${:.2}, {})", + println!(" - {} (${} USD, {})", product.name, product.price.amount, if product.is_purchasable() { "Available" } else { "Unavailable" } @@ -152,7 +178,7 @@ fn main() -> Result<(), Box> { // Insert the sale using model-specific methods db.insert_sale(&sale)?; - println!("Sale created: #{} for {} (${:.2})", + println!("Sale created: #{} for {} (${} USD)", sale.id, sale.buyer_name, sale.total_amount.amount @@ -171,7 +197,77 @@ fn main() -> Result<(), Box> { println!("Updated sale status to {:?}", retrieved_sale.status); - println!("\n6. Deleting Objects"); + println!("\n6. Working with Exchange Rates"); + println!("----------------------------"); + + // Create and set exchange rates using the builder + let eur_rate = ExchangeRateBuilder::new() + .base_currency("EUR") + .target_currency("USD") + .rate(1.18) + .build()?; + + let gbp_rate = ExchangeRateBuilder::new() + .base_currency("GBP") + .target_currency("USD") + .rate(1.38) + .build()?; + + // Insert exchange rates into the database + db.insert_exchange_rate(&eur_rate)?; + db.insert_exchange_rate(&gbp_rate)?; + + // Set the exchange rates in the service + EXCHANGE_RATE_SERVICE.set_rate(eur_rate.clone()); + EXCHANGE_RATE_SERVICE.set_rate(gbp_rate.clone()); + + println!("Exchange rates set:"); + println!(" - 1 EUR = {} USD", eur_rate.rate); + println!(" - 1 GBP = {} USD", gbp_rate.rate); + + // Create currencies in different denominations + let eur_price = CurrencyBuilder::new() + .amount(100.0) + .currency_code("EUR") + .build()?; + + let gbp_price = CurrencyBuilder::new() + .amount(85.0) + .currency_code("GBP") + .build()?; + + // Convert to USD + if let Some(eur_in_usd) = eur_price.to_usd() { + println!("{} EUR = {} USD", eur_price.amount, eur_in_usd.amount); + } else { + println!("Could not convert EUR to USD"); + } + + if let Some(gbp_in_usd) = gbp_price.to_usd() { + println!("{} GBP = {} USD", gbp_price.amount, gbp_in_usd.amount); + } else { + println!("Could not convert GBP to USD"); + } + + // Convert between currencies + if let Some(eur_in_gbp) = eur_price.to_currency("GBP") { + println!("{} EUR = {} GBP", eur_price.amount, eur_in_gbp.amount); + } else { + println!("Could not convert EUR to GBP"); + } + + // Test product price conversion + let retrieved_product2 = db.get_product(2)?; + + if let Some(price_in_eur) = retrieved_product2.cost_in_currency("EUR") { + println!("Product '{}' price: ${} USD = {} EUR", + retrieved_product2.name, + retrieved_product2.price.amount, + price_in_eur.amount + ); + } + + println!("\n7. Deleting Objects"); println!("------------------"); // Delete a product diff --git a/herodb/src/db/model_methods.rs b/herodb/src/db/model_methods.rs index 45fffd7..a284529 100644 --- a/herodb/src/db/model_methods.rs +++ b/herodb/src/db/model_methods.rs @@ -1,7 +1,7 @@ use crate::db::db::DB; use crate::db::base::{SledDBResult, SledModel}; use crate::impl_model_methods; -use crate::models::biz::{Product, Sale, Currency}; +use crate::models::biz::{Product, Sale, Currency, ExchangeRate}; // Implement model-specific methods for Product impl_model_methods!(Product, product, products); @@ -10,4 +10,7 @@ impl_model_methods!(Product, product, products); impl_model_methods!(Sale, sale, sales); // Implement model-specific methods for Currency -impl_model_methods!(Currency, currency, currencies); \ No newline at end of file +impl_model_methods!(Currency, currency, currencies); + +// Implement model-specific methods for ExchangeRate +impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates); \ No newline at end of file diff --git a/herodb/src/models/biz/currency.rs b/herodb/src/models/biz/currency.rs index 28464a0..cdf385d 100644 --- a/herodb/src/models/biz/currency.rs +++ b/herodb/src/models/biz/currency.rs @@ -1,6 +1,7 @@ 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; /// Currency represents a monetary value with amount and currency code #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,6 +18,26 @@ impl Currency { currency_code, } } + + /// Convert the currency to USD + pub fn to_usd(&self) -> Option { + if self.currency_code == "USD" { + return Some(self.clone()); + } + + EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, "USD") + .map(|amount| Currency::new(amount, "USD".to_string())) + } + + /// Convert the currency to another currency + pub fn to_currency(&self, target_currency: &str) -> Option { + if self.currency_code == target_currency { + return Some(self.clone()); + } + + EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, target_currency) + .map(|amount| Currency::new(amount, target_currency.to_string())) + } } /// Builder for Currency diff --git a/herodb/src/models/biz/exchange_rate.rs b/herodb/src/models/biz/exchange_rate.rs new file mode 100644 index 0000000..dd1c3e9 --- /dev/null +++ b/herodb/src/models/biz/exchange_rate.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::base::{SledModel, Storable}; + +/// ExchangeRate represents an exchange rate between two currencies +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRate { + pub base_currency: String, + pub target_currency: String, + pub rate: f64, + pub timestamp: DateTime, +} + +impl ExchangeRate { + /// Create a new exchange rate + pub fn new(base_currency: String, target_currency: String, rate: f64) -> Self { + Self { + base_currency, + target_currency, + rate, + timestamp: Utc::now(), + } + } +} + +/// Builder for ExchangeRate +pub struct ExchangeRateBuilder { + base_currency: Option, + target_currency: Option, + rate: Option, + timestamp: Option>, +} + +impl ExchangeRateBuilder { + /// Create a new ExchangeRateBuilder with all fields set to None + pub fn new() -> Self { + Self { + base_currency: None, + target_currency: None, + rate: None, + timestamp: None, + } + } + + /// Set the base currency + pub fn base_currency>(mut self, base_currency: S) -> Self { + self.base_currency = Some(base_currency.into()); + self + } + + /// Set the target currency + pub fn target_currency>(mut self, target_currency: S) -> Self { + self.target_currency = Some(target_currency.into()); + self + } + + /// Set the rate + pub fn rate(mut self, rate: f64) -> Self { + self.rate = Some(rate); + self + } + + /// Set the timestamp + pub fn timestamp(mut self, timestamp: DateTime) -> Self { + self.timestamp = Some(timestamp); + self + } + + /// Build the ExchangeRate object + pub fn build(self) -> Result { + let now = Utc::now(); + Ok(ExchangeRate { + base_currency: self.base_currency.ok_or("base_currency is required")?, + target_currency: self.target_currency.ok_or("target_currency is required")?, + rate: self.rate.ok_or("rate is required")?, + timestamp: self.timestamp.unwrap_or(now), + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for ExchangeRate {} + +// Implement SledModel trait +impl SledModel for ExchangeRate { + fn get_id(&self) -> String { + format!("{}_{}", self.base_currency, self.target_currency) + } + + fn db_prefix() -> &'static str { + "exchange_rate" + } +} + +/// ExchangeRateService provides methods to get and set exchange rates +#[derive(Clone)] +pub struct ExchangeRateService { + rates: Arc>>, +} + +impl ExchangeRateService { + /// Create a new exchange rate service + pub fn new() -> Self { + Self { + rates: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Set an exchange rate + pub fn set_rate(&self, exchange_rate: ExchangeRate) { + let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency); + let mut rates = self.rates.lock().unwrap(); + rates.insert(key, exchange_rate); + } + + /// Get an exchange rate + pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option { + let key = format!("{}_{}", base_currency, target_currency); + let rates = self.rates.lock().unwrap(); + rates.get(&key).cloned() + } + + /// Convert an amount from one currency to another + pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option { + // If the currencies are the same, return the amount + if from_currency == to_currency { + return Some(amount); + } + + // Try to get the direct exchange rate + if let Some(rate) = self.get_rate(from_currency, to_currency) { + return Some(amount * rate.rate); + } + + // Try to get the inverse exchange rate + if let Some(rate) = self.get_rate(to_currency, from_currency) { + return Some(amount / rate.rate); + } + + // Try to convert via USD + if from_currency != "USD" && to_currency != "USD" { + if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") { + return self.convert(from_to_usd, "USD", to_currency); + } + } + + None + } +} + +// Create a global instance of the exchange rate service +lazy_static::lazy_static! { + pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = { + let service = ExchangeRateService::new(); + + // Set some default exchange rates + service.set_rate(ExchangeRate::new("USD".to_string(), "EUR".to_string(), 0.85)); + service.set_rate(ExchangeRate::new("USD".to_string(), "GBP".to_string(), 0.75)); + service.set_rate(ExchangeRate::new("USD".to_string(), "JPY".to_string(), 110.0)); + service.set_rate(ExchangeRate::new("USD".to_string(), "CAD".to_string(), 1.25)); + service.set_rate(ExchangeRate::new("USD".to_string(), "AUD".to_string(), 1.35)); + + service + }; +} \ No newline at end of file diff --git a/herodb/src/models/biz/mod.rs b/herodb/src/models/biz/mod.rs index c7c3372..edf6306 100644 --- a/herodb/src/models/biz/mod.rs +++ b/herodb/src/models/biz/mod.rs @@ -1,13 +1,16 @@ pub mod currency; pub mod product; pub mod sale; +pub mod exchange_rate; // Re-export all model types for convenience pub use product::{Product, ProductComponent, ProductType, ProductStatus}; pub use sale::{Sale, SaleItem, SaleStatus}; pub use currency::Currency; +pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE}; // Re-export builder types pub use product::{ProductBuilder, ProductComponentBuilder}; pub use sale::{SaleBuilder, SaleItemBuilder}; -pub use currency::CurrencyBuilder; \ No newline at end of file +pub use currency::CurrencyBuilder; +pub use exchange_rate::ExchangeRateBuilder; \ No newline at end of file diff --git a/herodb/src/models/biz/product.rs b/herodb/src/models/biz/product.rs index 678f601..65d37a4 100644 --- a/herodb/src/models/biz/product.rs +++ b/herodb/src/models/biz/product.rs @@ -1,7 +1,7 @@ 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)] @@ -13,6 +13,10 @@ pub enum ProductType { /// ProductStatus represents the status of a product #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ProductStatus { + Active, + Error, + EndOfLife, + Paused, Available, Unavailable, } @@ -26,6 +30,8 @@ pub struct ProductComponent { 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 { @@ -39,8 +45,20 @@ impl ProductComponent { 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 @@ -51,6 +69,8 @@ pub struct ProductComponentBuilder { quantity: Option, created_at: Option>, updated_at: Option>, + energy_usage: Option, + cost: Option, } impl ProductComponentBuilder { @@ -63,6 +83,8 @@ impl ProductComponentBuilder { quantity: None, created_at: None, updated_at: None, + energy_usage: None, + cost: None, } } @@ -102,6 +124,18 @@ impl ProductComponentBuilder { 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(); @@ -112,6 +146,8 @@ impl ProductComponentBuilder { 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())), }) } } @@ -188,13 +224,60 @@ impl Product { /// Check if the product is available for purchase pub fn is_purchasable(&self) -> bool { - self.status == ProductStatus::Available && Utc::now() <= self.purchase_till + (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 @@ -355,4 +438,4 @@ impl SledModel for Product { } // Import Currency from the currency module -use crate::models::biz::Currency; +use crate::models::biz::Currency; \ No newline at end of file diff --git a/herodb/tmp/dbexample2/exchange_rate/conf b/herodb/tmp/dbexample2/exchange_rate/conf new file mode 100644 index 0000000..4154d7c --- /dev/null +++ b/herodb/tmp/dbexample2/exchange_rate/conf @@ -0,0 +1,4 @@ +segment_size: 524288 +use_compression: false +version: 0.34 +vQÁ \ No newline at end of file diff --git a/herodb/tmp/dbexample2/exchange_rate/db b/herodb/tmp/dbexample2/exchange_rate/db new file mode 100644 index 0000000..47e94fd Binary files /dev/null and b/herodb/tmp/dbexample2/exchange_rate/db differ diff --git a/herodb/tmp/dbexample2/product/db b/herodb/tmp/dbexample2/product/db index 3e682e3..c29f221 100644 Binary files a/herodb/tmp/dbexample2/product/db and b/herodb/tmp/dbexample2/product/db differ diff --git a/herodb/tmp/dbexample2/sale/db b/herodb/tmp/dbexample2/sale/db index a2c7114..0cfa073 100644 Binary files a/herodb/tmp/dbexample2/sale/db and b/herodb/tmp/dbexample2/sale/db differ