diff --git a/herodb/.gitignore b/herodb/.gitignore new file mode 100644 index 0000000..cedc139 --- /dev/null +++ b/herodb/.gitignore @@ -0,0 +1,5 @@ +target/ +temp/ +tmp/ +*.log +*.tmp diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index 5d297d8..795f874 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -19,7 +19,7 @@ tempfile = "3.8" poem = "1.3.55" poem-openapi = { version = "2.0.11", features = ["swagger-ui"] } tokio = { version = "1", features = ["full"] } -rhai = "1.15.1" +rhai = "1.21.0" paste = "1.0" lazy_static = "1.4.0" @@ -27,10 +27,22 @@ lazy_static = "1.4.0" name = "rhai_demo" path = "examples/rhai_demo.rs" +[[example]] +name = "business_models_demo" +path = "examples/business_models_demo.rs" + [[bin]] -name = "dbexample2" -path = "src/cmd/dbexample2/main.rs" +name = "dbexample_prod" +path = "src/cmd/dbexample_prod/main.rs" [[bin]] name = "dbexample_mcc" path = "src/cmd/dbexample_mcc/main.rs" + +[[bin]] +name = "dbexample_gov" +path = "src/cmd/dbexample_gov/main.rs" + +[[bin]] +name = "dbexample_biz" +path = "src/cmd/dbexample_biz/main.rs" diff --git a/herodb/README.md b/herodb/README.md index edbc14c..4075cfd 100644 --- a/herodb/README.md +++ b/herodb/README.md @@ -7,7 +7,12 @@ A database library built on top of sled with model support. ## example ```bash -cargo run --bin dbexample2 +#test for mcc module +cargo run --bin dbexample_mcc +#test for governance module +cargo run --bin dbexample_gov +#test for products +cargo run --bin dbexample_prod ``` ## Features diff --git a/herodb/examples/business_models_demo.rs b/herodb/examples/business_models_demo.rs new file mode 100644 index 0000000..31a46b8 --- /dev/null +++ b/herodb/examples/business_models_demo.rs @@ -0,0 +1,428 @@ +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; + +/// This example demonstrates business models in action: +/// 1. Defining products (2 types of server nodes) +/// 2. Defining components (parts of the nodes) +/// 3. Setting up pricing +/// 4. Creating a function to check which products can be bought +/// 5. Simulating a user buying a product +/// 6. Generating an invoice +/// 7. Simulating payment + +fn main() { + println!("Business Models Example"); + println!("=======================\n"); + + // Create a customer + let customer = create_customer(); + println!("Created customer: {}", customer.name); + + // Define products (server nodes) + let (standard_node, premium_node) = create_server_products(); + println!("Created server products:"); + println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code); + println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code); + + // Check which products can be purchased + println!("\nChecking which products can be purchased:"); + let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]); + for product in purchasable_products { + println!(" - {} is available for purchase", product.name); + } + + // Simulate a user buying a product + println!("\nSimulating purchase of a Premium Node:"); + let sale = create_sale(&customer, &premium_node); + println!(" - Sale created with ID: {}", sale.id); + println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code); + + // Generate an invoice + println!("\nGenerating invoice:"); + let invoice = create_invoice(&customer, &sale); + println!(" - Invoice created with ID: {}", invoice.id); + println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code); + println!(" - Due date: {}", invoice.due_date); + println!(" - Status: {:?}", invoice.status); + + // Simulate payment + println!("\nSimulating payment:"); + let paid_invoice = process_payment(invoice); + println!(" - Payment processed"); + println!(" - New balance due: ${} {}", paid_invoice.balance_due.amount, paid_invoice.balance_due.currency_code); + println!(" - Payment status: {:?}", paid_invoice.payment_status); + println!(" - Invoice status: {:?}", paid_invoice.status); + + println!("\nBusiness transaction completed successfully!"); +} + +// ===== Model Definitions ===== + +// Currency represents a monetary value with amount and currency code +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Currency { + amount: f64, + currency_code: String, +} + +// Customer represents a customer who can purchase products +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Customer { + id: u32, + name: String, + description: String, + pubkey: String, +} + +// ProductType represents the type of a product +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +enum ProductType { + Product, + Service, +} + +// ProductStatus represents the status of a product +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +enum ProductStatus { + Available, + Unavailable, +} + +// ProductComponent represents a component of a product +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ProductComponent { + id: i64, + name: String, + description: String, + quantity: i64, +} + +// Product represents a product or service offered +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Product { + id: i64, + name: String, + description: String, + price: Currency, + type_: ProductType, + category: String, + status: ProductStatus, + max_amount: i64, + purchase_till: DateTime, + active_till: DateTime, + components: Vec, +} + +// SaleStatus represents the status of a sale +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +enum SaleStatus { + Pending, + Completed, + Cancelled, +} + +// SaleItem represents an item in a sale +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SaleItem { + id: u32, + sale_id: u32, + product_id: u32, + name: String, + description: String, + comments: String, + quantity: i32, + unit_price: Currency, + subtotal: Currency, + tax_rate: f64, + tax_amount: Currency, + active_till: DateTime, +} + +// Sale represents a sale of products or services +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Sale { + id: u32, + company_id: u32, + customer_id: u32, + buyer_name: String, + buyer_email: String, + subtotal_amount: Currency, + tax_amount: Currency, + total_amount: Currency, + status: SaleStatus, + service_id: Option, + sale_date: DateTime, + items: Vec, +} + +// InvoiceStatus represents the status of an invoice +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +enum InvoiceStatus { + Draft, + Sent, + Paid, + Overdue, + Cancelled, +} + +// PaymentStatus represents the payment status of an invoice +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +enum PaymentStatus { + Unpaid, + PartiallyPaid, + Paid, +} + +// Payment represents a payment made against an invoice +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Payment { + amount: Currency, + date: DateTime, + method: String, + comment: String, +} + +// InvoiceItem represents an item in an invoice +#[derive(Debug, Clone, Serialize, Deserialize)] +struct InvoiceItem { + id: u32, + invoice_id: u32, + description: String, + amount: Currency, + sale_id: Option, +} + +// Invoice represents an invoice sent to a customer +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Invoice { + id: u32, + customer_id: u32, + total_amount: Currency, + balance_due: Currency, + status: InvoiceStatus, + payment_status: PaymentStatus, + issue_date: DateTime, + due_date: DateTime, + items: Vec, + payments: Vec, +} + +// ===== Implementation Functions ===== + +// Create a customer for our example +fn create_customer() -> Customer { + Customer { + id: 1, + name: "TechCorp Inc.".to_string(), + description: "Enterprise technology company".to_string(), + pubkey: "tech-corp-public-key-123".to_string(), + } +} + +// Create two types of server node products with their components +fn create_server_products() -> (Product, Product) { + let now = Utc::now(); + + // Create currency for pricing + let usd = |amount| { + Currency { + amount, + currency_code: "USD".to_string(), + } + }; + + // Standard Node Components + let cpu_standard = ProductComponent { + id: 1, + name: "CPU".to_string(), + description: "4-core CPU".to_string(), + quantity: 1, + }; + + let ram_standard = ProductComponent { + id: 2, + name: "RAM".to_string(), + description: "16GB RAM".to_string(), + quantity: 1, + }; + + let storage_standard = ProductComponent { + id: 3, + name: "Storage".to_string(), + description: "500GB SSD".to_string(), + quantity: 1, + }; + + // Premium Node Components + let cpu_premium = ProductComponent { + id: 4, + name: "CPU".to_string(), + description: "8-core CPU".to_string(), + quantity: 1, + }; + + let ram_premium = ProductComponent { + id: 5, + name: "RAM".to_string(), + description: "32GB RAM".to_string(), + quantity: 1, + }; + + let storage_premium = ProductComponent { + id: 6, + name: "Storage".to_string(), + description: "1TB SSD".to_string(), + quantity: 1, + }; + + let gpu_premium = ProductComponent { + id: 7, + name: "GPU".to_string(), + description: "Dedicated GPU".to_string(), + quantity: 1, + }; + + // Create Standard Node Product + let standard_node = Product { + id: 1, + name: "Standard Server Node".to_string(), + description: "Basic server node for general workloads".to_string(), + price: usd(99.99), + type_: ProductType::Product, + category: "Servers".to_string(), + status: ProductStatus::Available, + max_amount: 100, + purchase_till: now + Duration::days(365), + active_till: now + Duration::days(365), + components: vec![cpu_standard, ram_standard, storage_standard], + }; + + // Create Premium Node Product + let premium_node = Product { + id: 2, + name: "Premium Server Node".to_string(), + description: "High-performance server node for demanding workloads".to_string(), + price: usd(199.99), + type_: ProductType::Product, + category: "Servers".to_string(), + status: ProductStatus::Available, + max_amount: 50, + purchase_till: now + Duration::days(365), + active_till: now + Duration::days(365), + components: vec![cpu_premium, ram_premium, storage_premium, gpu_premium], + }; + + (standard_node, premium_node) +} + +// Check which products can be purchased +fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> { + products.iter() + .filter(|p| p.status == ProductStatus::Available && Utc::now() <= p.purchase_till) + .copied() + .collect() +} + +// Create a sale for a customer buying a product +fn create_sale(customer: &Customer, product: &Product) -> Sale { + let now = Utc::now(); + let active_till = now + Duration::days(365); + + // Create a sale item for the product + let sale_item = SaleItem { + id: 1, + sale_id: 1, + product_id: product.id as u32, + name: product.name.clone(), + description: product.description.clone(), + comments: "Customer requested expedited setup".to_string(), + quantity: 1, + unit_price: product.price.clone(), + subtotal: Currency { + amount: product.price.amount * 1.0, + currency_code: product.price.currency_code.clone(), + }, + tax_rate: 10.0, // 10% tax rate + tax_amount: Currency { + amount: product.price.amount * 0.1, + currency_code: product.price.currency_code.clone(), + }, + active_till, + }; + + // Calculate totals + let subtotal = sale_item.subtotal.clone(); + let tax_amount = sale_item.tax_amount.clone(); + let total_amount = Currency { + amount: subtotal.amount + tax_amount.amount, + currency_code: subtotal.currency_code.clone(), + }; + + // Create the sale + Sale { + id: 1, + company_id: 101, // Assuming company ID 101 + customer_id: customer.id, + buyer_name: customer.name.clone(), + buyer_email: "contact@techcorp.com".to_string(), // Example email + subtotal_amount: subtotal, + tax_amount, + total_amount, + status: SaleStatus::Completed, + service_id: None, + sale_date: now, + items: vec![sale_item], + } +} + +// Create an invoice for a sale +fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice { + let now = Utc::now(); + let due_date = now + Duration::days(30); // Due in 30 days + + // Create an invoice item for the sale + let invoice_item = InvoiceItem { + id: 1, + invoice_id: 1, + description: format!("Purchase of {}", sale.items[0].name), + amount: sale.total_amount.clone(), + sale_id: Some(sale.id), + }; + + // Create the invoice + Invoice { + id: 1, + customer_id: customer.id, + total_amount: sale.total_amount.clone(), + balance_due: sale.total_amount.clone(), + status: InvoiceStatus::Sent, + payment_status: PaymentStatus::Unpaid, + issue_date: now, + due_date, + items: vec![invoice_item], + payments: Vec::new(), + } +} + +// Process a payment for an invoice +fn process_payment(mut invoice: Invoice) -> Invoice { + // Create a payment for the full amount + let payment = Payment { + amount: invoice.total_amount.clone(), + date: Utc::now(), + method: "Credit Card".to_string(), + comment: "Payment received via credit card ending in 1234".to_string(), + }; + + // Add the payment to the invoice + invoice.payments.push(payment); + + // Update the balance due + invoice.balance_due.amount = 0.0; + + // Update the payment status + invoice.payment_status = PaymentStatus::Paid; + invoice.status = InvoiceStatus::Paid; + + invoice +} \ No newline at end of file diff --git a/herodb/examples/rhai_demo.rs b/herodb/examples/rhai_demo.rs deleted file mode 100644 index 1639e7c..0000000 --- a/herodb/examples/rhai_demo.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Demonstrates how to use the Rhai wrappers for our models - -use herodb::zaz::rhai::{run_script_file, run_example_script}; -use std::path::PathBuf; -use std::fs; -use std::time::SystemTime; - -fn main() -> Result<(), String> { - println!("=== RHAI MODEL WRAPPERS DEMONSTRATION ==="); - - // Run our test script that creates model objects - let test_script_path = "src/zaz/rhai/test.rhai"; - println!("\n1. Running model creation test script: {}", test_script_path); - run_script_file(test_script_path)?; - - // Create temporary directory for DB example - let temp_dir = create_temp_dir() - .map_err(|e| format!("Failed to create temp dir: {}", e))?; - - // Run our example script that uses the DB - println!("\n2. Running example with database at: {:?}", temp_dir); - run_example_script(temp_dir.to_str().unwrap())?; - - println!("\n=== DEMONSTRATION COMPLETED SUCCESSFULLY ==="); - Ok(()) -} - -/// Creates a simple temporary directory -fn create_temp_dir() -> std::io::Result { - let temp_dir = std::env::temp_dir(); - let random_name = format!("rhai-demo-{}", SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis()); - let path = temp_dir.join(random_name); - fs::create_dir_all(&path)?; - Ok(path) -} \ No newline at end of file diff --git a/herodb/src/cmd/dbexample/DB_README.md b/herodb/src/cmd/dbexample/DB_README.md deleted file mode 100644 index 25e312f..0000000 --- a/herodb/src/cmd/dbexample/DB_README.md +++ /dev/null @@ -1,91 +0,0 @@ -# HeroDB Architecture - -This document explains the architecture of HeroDB, focusing on the separation between model definitions and database logic. - -## Core Principles - -1. **Separation of Concerns**: The DB core should not know about specific models -2. **Registration-Based System**: Models get registered with the DB through a factory pattern -3. **Type-Safety**: Despite the separation, we maintain full type safety - -## Components - -### Core Module - -The `core` module provides the database foundation without knowing about specific models: - -- `SledModel` trait: Defines the interface models must implement -- `Storable` trait: Provides serialization/deserialization capabilities -- `SledDB`: Generic database wrapper for any model type -- `DB`: Main database manager that holds registered models -- `DBBuilder`: Builder for creating a DB with registered models - -### Zaz Module - -The `zaz` module contains domain-specific models and factories: - -- `models`: Defines specific model types like User, Company, etc. -- `factory`: Provides functions to create a DB with zaz models registered - -## Using the DB - -### Option 1: Factory Function - -The easiest way to create a DB with all zaz models is to use the factory: - -```rust -use herodb::zaz::create_zaz_db; - -// Create a DB with all zaz models registered -let db = create_zaz_db("/path/to/db")?; - -// Use the DB with specific model types -let user = User::new(...); -db.set(&user)?; -let retrieved: User = db.get(&id)?; -``` - -### Option 2: Builder Pattern - -For more control, use the builder pattern to register only the models you need: - -```rust -use herodb::core::{DBBuilder, DB}; -use herodb::zaz::models::{User, Company}; - -// Create a DB with only User and Company models -let db = DBBuilder::new("/path/to/db") - .register_model::() - .register_model::() - .build()?; -``` - -### Option 3: Dynamic Registration - -You can also register models with an existing DB: - -```rust -use herodb::core::DB; -use herodb::zaz::models::User; - -// Create an empty DB -let mut db = DB::new("/path/to/db")?; - -// Register the User model -db.register::()?; -``` - -## Benefits of this Architecture - -1. **Modularity**: The core DB code doesn't need to change when models change -2. **Extensibility**: New model types can be added without modifying core DB code -3. **Flexibility**: Different modules can define and use their own models with the same DB code -4. **Type Safety**: Full compile-time type checking is maintained - -## Implementation Details - -The key to this architecture is the combination of generic types and trait objects: - -- `SledDB` provides type-safe operations for specific model types -- `AnyDbOperations` trait allows type-erased operations through a common interface -- `TypeId` mapping enables runtime lookup of the correct DB for a given model type diff --git a/herodb/src/cmd/dbexample/db_tests.rs b/herodb/src/cmd/dbexample/db_tests.rs deleted file mode 100644 index b29dece..0000000 --- a/herodb/src/cmd/dbexample/db_tests.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Integration tests for zaz database module - -#[cfg(test)] -mod tests { - use sled; - use bincode; - use chrono::{DateTime, Utc}; - use serde::{Deserialize, Serialize}; - use std::path::Path; - use tempfile::tempdir; - use std::collections::HashMap; - - /// Test model for database operations - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] - struct User { - id: u32, - name: String, - email: String, - balance: f64, - created_at: DateTime, - updated_at: DateTime, - } - - impl User { - fn new(id: u32, name: String, email: String, balance: f64) -> Self { - let now = Utc::now(); - Self { - id, - name, - email, - balance, - created_at: now, - updated_at: now, - } - } - } - - /// Test basic CRUD operations - #[test] - fn test_basic_crud() { - // Create a temporary directory for testing - let temp_dir = tempdir().expect("Failed to create temp directory"); - println!("Created temporary directory at: {:?}", temp_dir.path()); - - // Open a sled database in the temporary directory - let db = sled::open(temp_dir.path().join("users")).expect("Failed to open database"); - println!("Opened database at: {:?}", temp_dir.path().join("users")); - - // CREATE a user - let user = User::new(1, "Test User".to_string(), "test@example.com".to_string(), 100.0); - let user_key = user.id.to_string(); - let user_value = bincode::serialize(&user).expect("Failed to serialize user"); - db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user"); - db.flush().expect("Failed to flush database"); - println!("Created user: {} ({})", user.name, user.email); - - // READ the user - let result = db.get(user_key.as_bytes()).expect("Failed to query database"); - assert!(result.is_some(), "User should exist"); - if let Some(data) = result { - let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user"); - println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email); - assert_eq!(user, retrieved_user, "Retrieved user should match original"); - } - - // UPDATE the user - let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string(), 150.0); - let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user"); - db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user"); - db.flush().expect("Failed to flush database"); - println!("Updated user: {} ({})", updated_user.name, updated_user.email); - - let result = db.get(user_key.as_bytes()).expect("Failed to query database"); - if let Some(data) = result { - let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user"); - assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version"); - } else { - panic!("User should exist after update"); - } - - // DELETE the user - db.remove(user_key.as_bytes()).expect("Failed to delete user"); - db.flush().expect("Failed to flush database"); - println!("Deleted user"); - - let result = db.get(user_key.as_bytes()).expect("Failed to query database"); - assert!(result.is_none(), "User should be deleted"); - - // Clean up - drop(db); - temp_dir.close().expect("Failed to cleanup temporary directory"); - } - - /// Test transaction-like behavior with multiple operations - #[test] - fn test_transaction_behavior() { - // Create a temporary directory for testing - let temp_dir = tempdir().expect("Failed to create temp directory"); - println!("Created temporary directory at: {:?}", temp_dir.path()); - - // Open a sled database in the temporary directory - let db = sled::open(temp_dir.path().join("tx_test")).expect("Failed to open database"); - println!("Opened transaction test database at: {:?}", temp_dir.path().join("tx_test")); - - // Create initial users - let user1 = User::new(1, "User One".to_string(), "one@example.com".to_string(), 100.0); - let user2 = User::new(2, "User Two".to_string(), "two@example.com".to_string(), 50.0); - - // Insert initial users - db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1).unwrap()).unwrap(); - db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2).unwrap()).unwrap(); - db.flush().unwrap(); - println!("Inserted initial users"); - - // Simulate a transaction - transfer 25.0 from user1 to user2 - println!("Starting transaction simulation: transfer 25.0 from user1 to user2"); - - // Create transaction workspace - let mut tx_workspace = HashMap::new(); - - // Retrieve current state - if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() { - let user: User = bincode::deserialize(&data).unwrap(); - tx_workspace.insert(user1.id.to_string(), user); - } - - if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() { - let user: User = bincode::deserialize(&data).unwrap(); - tx_workspace.insert(user2.id.to_string(), user); - } - - // Modify both users in the transaction - let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone(); - let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone(); - - updated_user1.balance -= 25.0; - updated_user2.balance += 25.0; - - // Update the workspace - tx_workspace.insert(user1.id.to_string(), updated_user1); - tx_workspace.insert(user2.id.to_string(), updated_user2); - - // Commit the transaction - println!("Committing transaction"); - for (key, user) in tx_workspace { - let user_bytes = bincode::serialize(&user).unwrap(); - db.insert(key.as_bytes(), user_bytes).unwrap(); - } - db.flush().unwrap(); - - // Verify the results - if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() { - let final_user1: User = bincode::deserialize(&data).unwrap(); - assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0"); - println!("Verified user1 balance is now {}", final_user1.balance); - } - - if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() { - let final_user2: User = bincode::deserialize(&data).unwrap(); - assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0"); - println!("Verified user2 balance is now {}", final_user2.balance); - } - - // Clean up - drop(db); - temp_dir.close().expect("Failed to cleanup temporary directory"); - } -} diff --git a/herodb/src/cmd/dbexample/factory.rs b/herodb/src/cmd/dbexample/factory.rs deleted file mode 100644 index b59c8d0..0000000 --- a/herodb/src/cmd/dbexample/factory.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Factory module for creating a DB with all zaz models registered - -use crate::core::{DB, DBBuilder, SledDBResult}; -use crate::zaz::models::*; -use std::path::PathBuf; - -/// Create a new DB instance with all zaz models registered -pub fn create_zaz_db>(path: P) -> SledDBResult { - // Using the builder pattern to register all models - DBBuilder::new(path) - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .build() -} - -/// Register all zaz models with an existing DB instance -pub fn register_zaz_models(db: &mut DB) -> SledDBResult<()> { - // Dynamically register all zaz models - db.register::()?; - db.register::()?; - db.register::()?; - db.register::()?; - db.register::()?; - db.register::()?; - db.register::()?; - - Ok(()) -} \ No newline at end of file diff --git a/herodb/src/cmd/dbexample2/main.rs b/herodb/src/cmd/dbexample2/main.rs deleted file mode 100644 index da68861..0000000 --- a/herodb/src/cmd/dbexample2/main.rs +++ /dev/null @@ -1,464 +0,0 @@ -use chrono::{Utc, Duration}; -use herodb::db::DBBuilder; -use herodb::models::biz::{ - Currency, CurrencyBuilder, - Product, ProductBuilder, ProductComponentBuilder, - ProductType, ProductStatus, - Sale, SaleBuilder, SaleItemBuilder, SaleStatus, - ExchangeRate, ExchangeRateBuilder, EXCHANGE_RATE_SERVICE, - Service, ServiceBuilder, ServiceItemBuilder, ServiceStatus, BillingFrequency, - Customer, CustomerBuilder, - Contract, ContractBuilder, ContractStatus, - Invoice, InvoiceBuilder, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment -}; -use std::path::PathBuf; -use std::fs; - -fn main() -> Result<(), Box> { - println!("DB Example 2: Using Builder Pattern and Model-Specific Methods"); - println!("============================================================"); - - // Create a temporary directory for the database - let db_path = PathBuf::from("/tmp/dbexample2"); - if db_path.exists() { - fs::remove_dir_all(&db_path)?; - } - fs::create_dir_all(&db_path)?; - println!("Database path: {:?}", db_path); - - // Create a database instance with our models registered - let db = DBBuilder::new(&db_path) - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .register_model::() - .build()?; - - println!("\n1. Creating Products with Builder Pattern"); - println!("----------------------------------------"); - - // Create a currency using the builder - let usd = CurrencyBuilder::new() - .amount(0.0) // Initial amount - .currency_code("USD") - .build()?; - - // Insert the currency - db.insert_currency(&usd)?; - println!("Currency created: ${} {}", usd.amount, usd.currency_code); - - // 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() - .id(102) - .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 - let product1 = ProductBuilder::new() - .id(1) - .name("Standard Plan") - .description("Our standard service offering") - .price(CurrencyBuilder::new() - .amount(29.99) - .currency_code("USD") - .build()?) - .type_(ProductType::Service) - .category("Subscription") - .status(ProductStatus::Active) - .max_amount(1000) - .validity_days(30) - .add_component(component1) - .build()?; - - let product2 = ProductBuilder::new() - .id(2) - .name("Premium Plan") - .description("Our premium service offering with priority support") - .price(CurrencyBuilder::new() - .amount(99.99) - .currency_code("USD") - .build()?) - .type_(ProductType::Service) - .category("Subscription") - .status(ProductStatus::Active) - .max_amount(500) - .validity_days(30) - .add_component(component2) - .build()?; - - // Insert products using model-specific methods - db.insert_product(&product1)?; - db.insert_product(&product2)?; - - 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: {} (${}) USD", retrieved_product1.name, retrieved_product1.price.amount); - println!("Components:"); - for component in &retrieved_product1.components { - 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"); - println!("----------------------"); - - // List all products using model-specific methods - let all_products = db.list_products()?; - println!("Found {} products:", all_products.len()); - for product in all_products { - println!(" - {} (${} USD, {})", - product.name, - product.price.amount, - if product.is_purchasable() { "Available" } else { "Unavailable" } - ); - } - - println!("\n4. Creating a Sale"); - println!("-----------------"); - - // Create a sale using the builder - let now = Utc::now(); - - let item1 = SaleItemBuilder::new() - .id(201) - .sale_id(1) - .product_id(1) - .name("Standard Plan") - .quantity(1) - .unit_price(CurrencyBuilder::new() - .amount(29.99) - .currency_code("USD") - .build()?) - .active_till(now + Duration::days(30)) - .build()?; - - let sale = SaleBuilder::new() - .id(1) - .company_id(101) - .buyer_name("John Doe") - .buyer_email("john.doe@example.com") - .currency_code("USD") - .status(SaleStatus::Pending) - .add_item(item1) - .build()?; - - // Insert the sale using model-specific methods - db.insert_sale(&sale)?; - println!("Sale created: #{} for {} (${} USD)", - sale.id, - sale.buyer_name, - sale.total_amount.amount - ); - - println!("\n5. Updating a Sale"); - println!("-----------------"); - - // Retrieve the sale, update it, and save it back - let mut retrieved_sale = db.get_sale(1)?; - println!("Retrieved sale: #{} with status {:?}", retrieved_sale.id, retrieved_sale.status); - - // Update the status - retrieved_sale.update_status(SaleStatus::Completed); - db.insert_sale(&retrieved_sale)?; - - println!("Updated sale status to {:?}", retrieved_sale.status); - - 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 - db.delete_product(2)?; - println!("Deleted product #2"); - - // List remaining products - let remaining_products = db.list_products()?; - println!("Remaining products: {}", remaining_products.len()); - for product in remaining_products { - println!(" - {}", product.name); - } - - println!("\n8. Creating a Customer"); - println!("--------------------"); - - // Create a customer using the builder - let customer = CustomerBuilder::new() - .id(1001) - .name("Jane Smith") - .description("Enterprise customer") - .pubkey("abc123def456") - .add_contact(5001) - .add_contact(5002) - .build()?; - - // Insert the customer - db.insert_customer(&customer)?; - println!("Customer created: {} (ID: {})", customer.name, customer.id); - println!("Contacts: {:?}", customer.contact_ids); - - println!("\n9. Creating a Service"); - println!("-------------------"); - - // Create service items using the builder - let service_item1 = ServiceItemBuilder::new() - .id(301) - .service_id(2001) - .product_id(1) - .name("Standard Plan - Monthly") - .quantity(1) - .unit_price(CurrencyBuilder::new() - .amount(29.99) - .currency_code("USD") - .build()?) - .tax_rate(0.07) // 7% tax - .is_taxable(true) - .active_till(now + Duration::days(30)) - .build()?; - - // Create a service using the builder - let service = ServiceBuilder::new() - .id(2001) - .customer_id(1001) - .currency_code("USD") - .status(ServiceStatus::Active) - .billing_frequency(BillingFrequency::Monthly) - .add_item(service_item1) - .build()?; - - // Insert the service - db.insert_service(&service)?; - println!("Service created: #{} for customer #{}", service.id, service.customer_id); - println!("Total amount: ${} USD (including tax)", service.total_amount.amount); - println!("Billing frequency: {:?}", service.billing_frequency); - - println!("\n10. Creating a Contract"); - println!("---------------------"); - - // Create a contract using the builder - let contract = ContractBuilder::new() - .id(3001) - .customer_id(1001) - .service_id(2001) - .terms("Monthly service contract with auto-renewal") - .start_date(now) - .end_date(now + Duration::days(365)) - .auto_renewal(true) - .renewal_terms("Renews automatically for 1 year unless cancelled 30 days prior") - .status(ContractStatus::Active) - .build()?; - - // Insert the contract - db.insert_contract(&contract)?; - println!("Contract created: #{} for customer #{}", contract.id, contract.customer_id); - println!("Contract period: {} to {}", - contract.start_date.format("%Y-%m-%d"), - contract.end_date.format("%Y-%m-%d") - ); - println!("Auto-renewal: {}", if contract.auto_renewal { "Yes" } else { "No" }); - - println!("\n11. Creating an Invoice"); - println!("---------------------"); - - // Create invoice items using the builder - let invoice_item1 = InvoiceItemBuilder::new() - .id(401) - .invoice_id(4001) - .description("Monthly service fee - Standard Plan") - .amount(CurrencyBuilder::new() - .amount(32.09) // Price with tax - .currency_code("USD") - .build()?) - .service_id(2001) - .build()?; - - // Create an invoice using the builder - let invoice = InvoiceBuilder::new() - .id(4001) - .customer_id(1001) - .currency_code("USD") - .issue_date(now) - .due_date(now + Duration::days(15)) - .add_item(invoice_item1) - .build()?; - - // Insert the invoice - db.insert_invoice(&invoice)?; - println!("Invoice created: #{} for customer #{}", invoice.id, invoice.customer_id); - println!("Total amount: ${} USD", invoice.total_amount.amount); - println!("Balance due: ${} USD", invoice.balance_due.amount); - println!("Status: {:?}, Payment status: {:?}", invoice.status, invoice.payment_status); - - println!("\n12. Processing a Payment"); - println!("----------------------"); - - // Retrieve the invoice, add a payment, and save it back - let mut retrieved_invoice = db.get_invoice(4001)?; - - // Create a payment - let payment = Payment::new( - CurrencyBuilder::new() - .amount(32.09) - .currency_code("USD") - .build()?, - "Credit Card".to_string() - ); - - // Add the payment to the invoice - retrieved_invoice.add_payment(payment); - - // Save the updated invoice - db.insert_invoice(&retrieved_invoice)?; - - println!("Payment processed for invoice #{}", retrieved_invoice.id); - println!("New balance due: ${} USD", retrieved_invoice.balance_due.amount); - println!("New status: {:?}, Payment status: {:?}", - retrieved_invoice.status, - retrieved_invoice.payment_status - ); - - println!("\n13. Retrieving Related Objects"); - println!("----------------------------"); - - // Retrieve customer and related objects - let retrieved_customer = db.get_customer(1001)?; - println!("Customer: {} (ID: {})", retrieved_customer.name, retrieved_customer.id); - - // Retrieve service for this customer - let retrieved_service = db.get_service(2001)?; - println!("Service: #{} with {} items", - retrieved_service.id, - retrieved_service.items.len() - ); - - // Retrieve contract for this customer - let retrieved_contract = db.get_contract(3001)?; - println!("Contract: #{} ({})", - retrieved_contract.id, - if retrieved_contract.is_active() { "Active" } else { "Inactive" } - ); - - // Retrieve invoice for this customer - let retrieved_invoice = db.get_invoice(4001)?; - println!("Invoice: #{} ({})", - retrieved_invoice.id, - match retrieved_invoice.payment_status { - PaymentStatus::Paid => "Paid", - PaymentStatus::PartiallyPaid => "Partially Paid", - PaymentStatus::Unpaid => "Unpaid", - } - ); - - println!("\nExample completed successfully!"); - Ok(()) -} \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_biz/README.md b/herodb/src/cmd/dbexample_biz/README.md new file mode 100644 index 0000000..37cc41c --- /dev/null +++ b/herodb/src/cmd/dbexample_biz/README.md @@ -0,0 +1,48 @@ +# Business Models Example + +This example demonstrates the business models in HeroDB, showcasing a complete business transaction flow from product definition to payment processing. + +## Features Demonstrated + +1. **Product Definition**: Creating two types of server node products with different components and pricing +2. **Component Definition**: Defining the parts that make up each server node (CPU, RAM, Storage, GPU) +3. **Pricing Setup**: Setting up prices for products using the Currency model +4. **Product Availability**: Checking which products can be purchased based on their status and availability +5. **Sales Process**: Simulating a customer purchasing a product +6. **Invoice Generation**: Creating an invoice for the sale +7. **Payment Processing**: Processing a payment for the invoice and updating its status + +## Business Flow + +The example follows this business flow: + +``` +Define Products → Check Availability → Customer Purchase → Generate Invoice → Process Payment +``` + +## Models Used + +- **Product & ProductComponent**: For defining server nodes and their components +- **Customer**: For representing the buyer +- **Sale & SaleItem**: For recording the purchase transaction +- **Invoice & InvoiceItem**: For billing the customer +- **Payment**: For recording the payment + +## Running the Example + +To run this example, use: + +```bash +cargo run --bin dbexample_biz +``` + +The output will show each step of the business process with relevant details. + +## Key Concepts + +- **Builder Pattern**: All models use builders for flexible object creation +- **Status Tracking**: Sales and invoices have status enums to track their state +- **Relationship Modeling**: The example shows how different business entities relate to each other +- **Financial Calculations**: Demonstrates tax and total calculations + +This example provides a template for implementing business logic in your own applications using HeroDB. \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_biz/main.rs b/herodb/src/cmd/dbexample_biz/main.rs new file mode 100644 index 0000000..c03c079 --- /dev/null +++ b/herodb/src/cmd/dbexample_biz/main.rs @@ -0,0 +1,275 @@ +use chrono::{Duration, Utc}; +use crate::models::biz::{ + Currency, CurrencyBuilder, + Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus, + Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus, + Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, Payment, PaymentStatus, + Customer, CustomerBuilder, +}; +use crate::db::base::SledModel; + +/// This example demonstrates the business models in action: +/// 1. Defining products (2 types of server nodes) +/// 2. Defining components (parts of the nodes) +/// 3. Setting up pricing +/// 4. Creating a function to check which products can be bought +/// 5. Simulating a user buying a product +/// 6. Generating an invoice +/// 7. Simulating payment + +fn main() { + println!("Business Models Example"); + println!("=======================\n"); + + // Create a customer + let customer = create_customer(); + println!("Created customer: {}", customer.name); + + // Define products (server nodes) + let (standard_node, premium_node) = create_server_products(); + println!("Created server products:"); + println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code); + println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code); + + // Check which products can be purchased + println!("\nChecking which products can be purchased:"); + let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]); + for product in purchasable_products { + println!(" - {} is available for purchase", product.name); + } + + // Simulate a user buying a product + println!("\nSimulating purchase of a Premium Node:"); + let sale = create_sale(&customer, &premium_node); + println!(" - Sale created with ID: {}", sale.id); + println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code); + + // Generate an invoice + println!("\nGenerating invoice:"); + let invoice = create_invoice(&customer, &sale); + println!(" - Invoice created with ID: {}", invoice.id); + println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code); + println!(" - Due date: {}", invoice.due_date); + println!(" - Status: {:?}", invoice.status); + + // Simulate payment + println!("\nSimulating payment:"); + let paid_invoice = process_payment(invoice); + println!(" - Payment processed"); + println!(" - New balance due: ${} {}", paid_invoice.balance_due.amount, paid_invoice.balance_due.currency_code); + println!(" - Payment status: {:?}", paid_invoice.payment_status); + println!(" - Invoice status: {:?}", paid_invoice.status); + + println!("\nBusiness transaction completed successfully!"); +} + +/// Create a customer for our example +fn create_customer() -> Customer { + CustomerBuilder::new() + .id(1) + .name("TechCorp Inc.") + .description("Enterprise technology company") + .pubkey("tech-corp-public-key-123") + .build() + .expect("Failed to create customer") +} + +/// Create two types of server node products with their components +fn create_server_products() -> (Product, Product) { + // Create currency for pricing + let usd = |amount| { + CurrencyBuilder::new() + .amount(amount) + .currency_code("USD") + .build() + .expect("Failed to create currency") + }; + + // Standard Node Components + let cpu_standard = ProductComponentBuilder::new() + .id(1) + .name("CPU") + .description("4-core CPU") + .quantity(1) + .build() + .expect("Failed to create CPU component"); + + let ram_standard = ProductComponentBuilder::new() + .id(2) + .name("RAM") + .description("16GB RAM") + .quantity(1) + .build() + .expect("Failed to create RAM component"); + + let storage_standard = ProductComponentBuilder::new() + .id(3) + .name("Storage") + .description("500GB SSD") + .quantity(1) + .build() + .expect("Failed to create Storage component"); + + // Premium Node Components + let cpu_premium = ProductComponentBuilder::new() + .id(4) + .name("CPU") + .description("8-core CPU") + .quantity(1) + .build() + .expect("Failed to create CPU component"); + + let ram_premium = ProductComponentBuilder::new() + .id(5) + .name("RAM") + .description("32GB RAM") + .quantity(1) + .build() + .expect("Failed to create RAM component"); + + let storage_premium = ProductComponentBuilder::new() + .id(6) + .name("Storage") + .description("1TB SSD") + .quantity(1) + .build() + .expect("Failed to create Storage component"); + + let gpu_premium = ProductComponentBuilder::new() + .id(7) + .name("GPU") + .description("Dedicated GPU") + .quantity(1) + .build() + .expect("Failed to create GPU component"); + + // Create Standard Node Product + let standard_node = ProductBuilder::new() + .id(1) + .name("Standard Server Node") + .description("Basic server node for general workloads") + .price(usd(99.99)) + .type_(ProductType::Product) + .category("Servers") + .status(ProductStatus::Available) + .max_amount(100) + .validity_days(365) + .add_component(cpu_standard) + .add_component(ram_standard) + .add_component(storage_standard) + .build() + .expect("Failed to create Standard Node product"); + + // Create Premium Node Product + let premium_node = ProductBuilder::new() + .id(2) + .name("Premium Server Node") + .description("High-performance server node for demanding workloads") + .price(usd(199.99)) + .type_(ProductType::Product) + .category("Servers") + .status(ProductStatus::Available) + .max_amount(50) + .validity_days(365) + .add_component(cpu_premium) + .add_component(ram_premium) + .add_component(storage_premium) + .add_component(gpu_premium) + .build() + .expect("Failed to create Premium Node product"); + + (standard_node, premium_node) +} + +/// Check which products can be purchased +fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> { + products.iter() + .filter(|p| p.is_purchasable()) + .copied() + .collect() +} + +/// Create a sale for a customer buying a product +fn create_sale(customer: &Customer, product: &Product) -> Sale { + let now = Utc::now(); + let active_till = now + Duration::days(365); + + // Create a sale item for the product + let sale_item = SaleItemBuilder::new() + .id(1) + .sale_id(1) + .product_id(product.id as u32) + .name(product.name.clone()) + .description(product.description.clone()) + .comments("Customer requested expedited setup") + .quantity(1) + .unit_price(product.price.clone()) + .tax_rate(10.0) // 10% tax rate + .active_till(active_till) + .build() + .expect("Failed to create sale item"); + + // Create the sale + let sale = SaleBuilder::new() + .id(1) + .company_id(101) // Assuming company ID 101 + .customer_id(customer.id) + .buyer_name(customer.name.clone()) + .buyer_email("contact@techcorp.com") // Example email + .currency_code(product.price.currency_code.clone()) + .status(SaleStatus::Completed) + .add_item(sale_item) + .build() + .expect("Failed to create sale"); + + sale +} + +/// Create an invoice for a sale +fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice { + let now = Utc::now(); + let due_date = now + Duration::days(30); // Due in 30 days + + // Create an invoice item for the sale + let invoice_item = InvoiceItemBuilder::new() + .id(1) + .invoice_id(1) + .description(format!("Purchase of {}", sale.items[0].name)) + .amount(sale.total_amount.clone()) + .sale_id(sale.id) + .build() + .expect("Failed to create invoice item"); + + // Create the invoice + let invoice = InvoiceBuilder::new() + .id(1) + .customer_id(customer.id) + .currency_code(sale.total_amount.currency_code.clone()) + .status(InvoiceStatus::Sent) + .issue_date(now) + .due_date(due_date) + .add_item(invoice_item) + .build() + .expect("Failed to create invoice"); + + invoice +} + +/// Process a payment for an invoice +fn process_payment(mut invoice: Invoice) -> Invoice { + // Create a payment for the full amount + let payment = Payment::new( + invoice.total_amount.clone(), + "Credit Card".to_string(), + "Payment received via credit card ending in 1234".to_string() + ); + + // Add the payment to the invoice + invoice.add_payment(payment); + + // The invoice should now be marked as paid + assert_eq!(invoice.payment_status, PaymentStatus::Paid); + assert_eq!(invoice.status, InvoiceStatus::Paid); + + invoice +} \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_biz/mod.rs b/herodb/src/cmd/dbexample_biz/mod.rs new file mode 100644 index 0000000..bcc5b77 --- /dev/null +++ b/herodb/src/cmd/dbexample_biz/mod.rs @@ -0,0 +1,10 @@ +//! Business example for HeroDB +//! +//! This module demonstrates business models in action, +//! including products, sales, invoices, and payments. + +// Re-export the main function +pub use self::main::*; + +// Include the main module +mod main; \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_governance/main.rs b/herodb/src/cmd/dbexample_gov/main.rs similarity index 94% rename from herodb/src/cmd/dbexample_governance/main.rs rename to herodb/src/cmd/dbexample_gov/main.rs index 1758f4b..1578a90 100644 --- a/herodb/src/cmd/dbexample_governance/main.rs +++ b/herodb/src/cmd/dbexample_gov/main.rs @@ -1,6 +1,6 @@ use chrono::{Utc, Duration}; -use herodb::db::DBBuilder; -use herodb::models::governance::{ +use herodb::db::{DBBuilder, SledDB, SledModel}; +use herodb::models::gov::{ Company, CompanyStatus, BusinessType, Shareholder, ShareholderType, Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus, @@ -12,11 +12,11 @@ use std::path::PathBuf; use std::fs; fn main() -> Result<(), Box> { - println!("DB Example: Governance Module"); + println!("DB Example: Gov Module"); println!("============================"); // Create a temporary directory for the database - let db_path = PathBuf::from("/tmp/dbexample_governance"); + let db_path = PathBuf::from("/tmp/dbexample_gov"); if db_path.exists() { fs::remove_dir_all(&db_path)?; } @@ -58,7 +58,7 @@ fn main() -> Result<(), Box> { ); // Insert the company - db.insert(&company)?; + db.set(&company)?; println!("Company created: {} (ID: {})", company.name, company.id); println!("Status: {:?}, Business Type: {}", company.status, company.business_type.as_str()); @@ -94,9 +94,9 @@ fn main() -> Result<(), Box> { ); // Insert the users - db.insert(&user1)?; - db.insert(&user2)?; - db.insert(&user3)?; + db.set(&user1)?; + db.set(&user2)?; + db.set(&user3)?; println!("User created: {} ({})", user1.name, user1.role); println!("User created: {} ({})", user2.name, user2.role); @@ -137,9 +137,9 @@ fn main() -> Result<(), Box> { ); // Insert the shareholders - db.insert(&shareholder1)?; - db.insert(&shareholder2)?; - db.insert(&shareholder3)?; + db.set(&shareholder1)?; + db.set(&shareholder2)?; + db.set(&shareholder3)?; println!("Shareholder created: {} ({} shares, {}%)", shareholder1.name, shareholder1.shares, shareholder1.percentage); @@ -150,7 +150,7 @@ fn main() -> Result<(), Box> { // Update shareholder shares shareholder1.update_shares(1100.0, 44.0); - db.insert(&shareholder1)?; + db.set(&shareholder1)?; println!("Updated shareholder: {} ({} shares, {}%)", shareholder1.name, shareholder1.shares, shareholder1.percentage); @@ -198,7 +198,7 @@ fn main() -> Result<(), Box> { meeting.add_attendee(attendee3); // Insert the meeting - db.insert(&meeting)?; + db.set(&meeting)?; println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M")); println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len()); @@ -209,7 +209,7 @@ fn main() -> Result<(), Box> { if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) { attendee.update_status(AttendeeStatus::Confirmed); } - db.insert(&meeting)?; + db.set(&meeting)?; // Get confirmed attendees let confirmed = meeting.confirmed_attendees(); @@ -242,19 +242,19 @@ fn main() -> Result<(), Box> { resolution.link_to_meeting(meeting.id); // Insert the resolution - db.insert(&resolution)?; + db.set(&resolution)?; println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status); // Propose the resolution resolution.propose(); - db.insert(&resolution)?; + db.set(&resolution)?; println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d")); // Add approvals resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string()); resolution.add_approval(user2.id, user2.name.clone(), true, "Financials look good".to_string()); resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string()); - db.insert(&resolution)?; + db.set(&resolution)?; // Check approval status println!("Approvals: {}, Rejections: {}", @@ -263,7 +263,7 @@ fn main() -> Result<(), Box> { // Approve the resolution resolution.approve(); - db.insert(&resolution)?; + db.set(&resolution)?; println!("Resolution approved on {}", resolution.approved_at.unwrap().format("%Y-%m-%d")); @@ -287,7 +287,7 @@ fn main() -> Result<(), Box> { vote.add_option("Abstain".to_string(), 0); // Insert the vote - db.insert(&vote)?; + db.set(&vote)?; println!("Vote created: {} (Status: {:?})", vote.title, vote.status); println!("Voting period: {} to {}", vote.start_date.format("%Y-%m-%d"), @@ -297,7 +297,7 @@ fn main() -> Result<(), Box> { vote.add_ballot(user1.id, 1, 1000); // User 1 votes "Approve" with 1000 shares vote.add_ballot(user2.id, 1, 750); // User 2 votes "Approve" with 750 shares vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares - db.insert(&vote)?; + db.set(&vote)?; // Check voting results println!("Voting results:"); @@ -318,7 +318,7 @@ fn main() -> Result<(), Box> { // Link the resolution to the vote vote_resolution.link_to_vote(vote.id); vote_resolution.propose(); - db.insert(&vote_resolution)?; + db.set(&vote_resolution)?; println!("Created resolution linked to vote: {}", vote_resolution.title); println!("\n7. Retrieving Related Objects"); diff --git a/herodb/src/cmd/dbexample_governance/Cargo.lock b/herodb/src/cmd/dbexample_governance/Cargo.lock deleted file mode 100644 index cddb29b..0000000 --- a/herodb/src/cmd/dbexample_governance/Cargo.lock +++ /dev/null @@ -1,2161 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.2.15", - "once_cell", - "version_check", - "zerocopy 0.7.35", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "async-trait" -version = "0.1.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "cc" -version = "1.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.15", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "aes-gcm", - "base64", - "hkdf", - "hmac", - "percent-encoding", - "rand", - "sha2", - "subtle", - "time", - "version_check", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "dbexample_governance" -version = "0.1.0" -dependencies = [ - "chrono", - "herodb", -] - -[[package]] -name = "deranged" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "0.99.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.100", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-macro", - "futures-sink", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.8.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - -[[package]] -name = "herodb" -version = "0.1.0" -dependencies = [ - "bincode", - "brotli", - "chrono", - "lazy_static", - "paste", - "poem", - "poem-openapi", - "rhai", - "serde", - "serde_json", - "sled", - "tempfile", - "thiserror", - "tokio", - "uuid", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "linux-raw-sys" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin", - "tokio", - "version_check", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.9.0", - "cfg-if", - "libc", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.10", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "poem" -version = "1.3.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504774c97b0744c1ee108a37e5a65a9745a4725c4c06277521dabc28eb53a904" -dependencies = [ - "async-trait", - "bytes", - "chrono", - "cookie", - "futures-util", - "headers", - "http", - "hyper", - "mime", - "multer", - "nix", - "parking_lot 0.12.3", - "percent-encoding", - "pin-project-lite", - "poem-derive", - "quick-xml 0.30.0", - "regex", - "rfc7239", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "smallvec", - "tempfile", - "thiserror", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "wildmatch", -] - -[[package]] -name = "poem-derive" -version = "1.3.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ddcf4680d8d867e1e375116203846acb088483fa2070244f90589f458bbb31" -dependencies = [ - "proc-macro-crate 2.0.2", - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "poem-openapi" -version = "2.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e26f78b6195ea1b7a16f46bda1961c598e5a66912f2aa1b8b7a2f395aebb9fc" -dependencies = [ - "base64", - "bytes", - "derive_more", - "futures-util", - "mime", - "num-traits", - "poem", - "poem-openapi-derive", - "quick-xml 0.26.0", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "thiserror", - "tokio", -] - -[[package]] -name = "poem-openapi-derive" -version = "2.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c3e2975c930dc72c024e75b230c3b6058fb3a746d5739b83aa8f28ab1a42d4" -dependencies = [ - "darling", - "http", - "indexmap 1.9.3", - "mime", - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "thiserror", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy 0.8.24", -] - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rfc7239" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a82f1d1e38e9a85bb58ffcfadf22ed6f2c94e8cd8581ec2b0f80a2a6858350f" -dependencies = [ - "uncased", -] - -[[package]] -name = "rhai" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" -dependencies = [ - "ahash", - "bitflags 2.9.0", - "instant", - "num-traits", - "once_cell", - "rhai_codegen", - "smallvec", - "smartstring", - "thin-vec", -] - -[[package]] -name = "rhai_codegen" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.8.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "socket2" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" -dependencies = [ - "fastrand", - "getrandom 0.3.2", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thin-vec" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tokio" -version = "1.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot 0.12.3", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.8.0", - "toml_datetime", - "winnow", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.8.0", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" -dependencies = [ - "getrandom 0.3.2", - "serde", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.100", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wildmatch" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.61.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-result" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] diff --git a/herodb/src/cmd/dbexample_governance/Cargo.toml b/herodb/src/cmd/dbexample_governance/Cargo.toml deleted file mode 100644 index 0b589b7..0000000 --- a/herodb/src/cmd/dbexample_governance/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "dbexample_governance" -version = "0.1.0" -edition = "2021" - -[[bin]] -name = "dbexample_governance" -path = "main.rs" - -[dependencies] -herodb = { path = "../../.." } -chrono = "0.4" \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_prod/main.rs b/herodb/src/cmd/dbexample_prod/main.rs new file mode 100644 index 0000000..b7cf3fe --- /dev/null +++ b/herodb/src/cmd/dbexample_prod/main.rs @@ -0,0 +1,360 @@ +use chrono::{DateTime, Duration, Utc}; +use herodb::db::{DB, DBBuilder}; +use herodb::models::biz::{ + Currency, CurrencyBuilder, Product, ProductBuilder, ProductComponent, ProductComponentBuilder, + ProductStatus, ProductType, Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus, +}; +use rhai::{Engine, packages::Package}; +use std::fs; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + println!("DB Example 2: Using Builder Pattern and Model-Specific Methods"); + println!("============================================================"); + + // Create a temporary directory for the database + let db_path = PathBuf::from("/tmp/dbexample_prod"); + if db_path.exists() { + fs::remove_dir_all(&db_path)?; + } + fs::create_dir_all(&db_path)?; + println!("Database path: {:?}", db_path); + + let mut engine = Engine::new(); + + engine + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::(); + + // Register currency builder methods + engine.register_fn("new_currency_builder", CurrencyBuilder::new); + engine.register_fn("amount", CurrencyBuilder::amount); + engine.register_fn("currency_code", CurrencyBuilder::currency_code::); + engine.register_fn("build", CurrencyBuilder::build); + + // Register method to verify currency + engine.register_fn("amount", Currency::amount); + + // Register product component builder methods + engine.register_fn( + "new_product_component_builder", + ProductComponentBuilder::new, + ); + engine.register_fn("id", ProductComponentBuilder::id); + engine.register_fn("name", ProductComponentBuilder::name::); + engine.register_fn( + "description", + ProductComponentBuilder::description::, + ); + engine.register_fn("quantity", ProductComponentBuilder::quantity); + engine.register_fn("build", ProductComponentBuilder::build); + + // Register product builder methods + engine.register_fn("new_product_builder", ProductBuilder::new); + engine.register_fn("id", ProductBuilder::id); + engine.register_fn("name", ProductBuilder::name::); + engine.register_fn("description", ProductBuilder::description::); + engine.register_fn("price", ProductBuilder::price); + engine.register_fn("type", ProductBuilder::type_); + engine.register_fn("category", ProductBuilder::category::); + engine.register_fn("status", ProductBuilder::status); + engine.register_fn("max_amount", ProductBuilder::max_amount); + engine.register_fn("validity_days", ProductBuilder::validity_days); + engine.register_fn("add_component", ProductBuilder::add_component); + engine.register_fn("build", ProductBuilder::build); + + // Register db builder methods + engine.register_fn("new_db_builder", DBBuilder::new::); + engine.register_fn("register_currency", DBBuilder::register_model::); + engine.register_fn("register_product", DBBuilder::register_model::); + engine.register_fn("register_sale", DBBuilder::register_model::); + engine.register_fn("currency_code", CurrencyBuilder::currency_code::); + engine.register_fn("build", DBBuilder::build); + + // Register db methods + engine.register_fn("insert_currency", DB::insert_currency); + engine.register_fn("insert_product", DB::insert_product); + + let script = r#" + let usd = new_currency_builder() + .amount(0.0) + .currency_code("USD") + .build(); + + // Can we access and print this from the actual Currency? + print(usd.amount()); + + let db = new_db_builder("./tmp/dbexample2") + .register_product() + .register_currency() + .register_sale() + .build(); + + db.insert_currency(usd); + + let component1 = new_product_component_builder() + .id(101) + .name("Basic Support") + .description("24/7 email support") + .quantity(1) + .build(); + + let component2 = new_product_component_builder() + .id(102) + .name("Premium Support") + .description("24/7 phone and email support") + .quantity(1) + .build(); + + // Create products using the builder + // let product1 = new_product_builder() + // .id(1) + // .name("Standard Plan") + // .description("Our standard service offering") + // .price( + // new_currency_builder() + // .amount(29.99) + // .currency_code("USD") + // .build() + // ) + // .type_(ProductType::Service) + // .category("Subscription") + // .status(ProductStatus::Available) + // .max_amount(1000) + // .validity_days(30) + // .add_component(component1) + // .build(); + // + // let product2 = new_product_builder() + // .id(2) + // .name("Premium Plan") + // .description("Our premium service offering with priority support") + // .price( + // new_currency_builder() + // .amount(99.99) + // .currency_code("USD") + // .build() + // ) + // .type_(ProductType::Service) + // .category("Subscription") + // .status(ProductStatus::Available) + // .max_amount(500) + // .validity_days(30) + // .add_component(component2) + // .build(); + + // Insert products using model-specific methods + // db.insert_product(product1); + // db.insert_product(product2); + "#; + + + println!("\n0. Executing Script"); + println!("----------------------------------------"); + + + engine.eval::<()>(script)?; + + // Create a database instance with our models registered + let mut db = DBBuilder::new(&db_path) + .register_model::() + .register_model::() + .register_model::() + .build()?; + + // Check if the currency created in the script is actually present, if it is this value should + // be 1 (NOTE: it will be :) ). + let currencies = db.list_currencies()?; + println!("Found {} currencies in db", currencies.len()); + for currency in currencies { + println!("{} {}", currency.amount, currency.currency_code); + } + + println!("\n1. Creating Products with Builder Pattern"); + println!("----------------------------------------"); + + // // Create a currency using the builder + // let usd = CurrencyBuilder::new() + // .amount(0.0) // Initial amount + // .currency_code("USD") + // .build()?; + // + // // Insert the currency + // db.insert_currency(usd.clone())?; + // println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code); + + // Create product components using the builder + let component1 = ProductComponentBuilder::new() + .id(101) + .name("Basic Support") + .description("24/7 email support") + .quantity(1) + .build()?; + + let component2 = ProductComponentBuilder::new() + .id(102) + .name("Premium Support") + .description("24/7 phone and email support") + .quantity(1) + .build()?; + // Create products using the builder + let product1 = ProductBuilder::new() + .id(1) + .name("Standard Plan") + .description("Our standard service offering") + .price( + CurrencyBuilder::new() + .amount(29.99) + .currency_code("USD") + .build()?, + ) + .type_(ProductType::Service) + .category("Subscription") + .status(ProductStatus::Available) + .max_amount(1000) + .validity_days(30) + .add_component(component1) + .build()?; + + let product2 = ProductBuilder::new() + .id(2) + .name("Premium Plan") + .description("Our premium service offering with priority support") + .price( + CurrencyBuilder::new() + .amount(99.99) + .currency_code("USD") + .build()?, + ) + .type_(ProductType::Service) + .category("Subscription") + .status(ProductStatus::Available) + .max_amount(500) + .validity_days(30) + .add_component(component2) + .build()?; + + // Insert products using model-specific methods + db.insert_product(product1.clone())?; + db.insert_product(product2.clone())?; + + println!( + "Product created: {} (${:.2})", + product1.name, product1.price.amount + ); + println!( + "Product created: {} (${:.2})", + 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!("Components:"); + for component in &retrieved_product1.components { + println!(" - {} ({})", component.name, component.description); + } + + println!("\n3. Listing All Products"); + println!("----------------------"); + + // List all products using model-specific methods + let all_products = db.list_products()?; + println!("Found {} products:", all_products.len()); + for product in all_products { + println!( + " - {} (${:.2}, {})", + product.name, + product.price.amount, + if product.is_purchasable() { + "Available" + } else { + "Unavailable" + } + ); + } + + println!("\n4. Creating a Sale"); + println!("-----------------"); + + // Create a sale using the builder + let now = Utc::now(); + + let item1 = SaleItemBuilder::new() + .id(201) + .sale_id(1) + .product_id(1) + .name("Standard Plan") + .quantity(1) + .unit_price( + CurrencyBuilder::new() + .amount(29.99) + .currency_code("USD") + .build()?, + ) + .active_till(now + Duration::days(30)) + .build()?; + + let sale = SaleBuilder::new() + .id(1) + .company_id(101) + .buyer_name("John Doe") + .buyer_email("john.doe@example.com") + .currency_code("USD") + .status(SaleStatus::Pending) + .add_item(item1) + .build()?; + + // Insert the sale using model-specific methods + db.insert_sale(sale.clone())?; + println!( + "Sale created: #{} for {} (${:.2})", + sale.id, sale.buyer_name, sale.total_amount.amount + ); + + println!("\n5. Updating a Sale"); + println!("-----------------"); + + // Retrieve the sale, update it, and save it back + let mut retrieved_sale = db.get_sale(1)?; + println!( + "Retrieved sale: #{} with status {:?}", + retrieved_sale.id, retrieved_sale.status + ); + + // Update the status + retrieved_sale.update_status(SaleStatus::Completed); + db.insert_sale(retrieved_sale.clone())?; + + println!("Updated sale status to {:?}", retrieved_sale.status); + + println!("\n6. Deleting Objects"); + println!("------------------"); + + // Delete a product + db.delete_product(2)?; + println!("Deleted product #2"); + + // List remaining products + let remaining_products = db.list_products()?; + println!("Remaining products: {}", remaining_products.len()); + for product in remaining_products { + println!(" - {}", product.name); + } + + println!("\nExample completed successfully!"); + Ok(()) +} diff --git a/herodb/src/cmd/mod.rs b/herodb/src/cmd/mod.rs new file mode 100644 index 0000000..71b73ae --- /dev/null +++ b/herodb/src/cmd/mod.rs @@ -0,0 +1,7 @@ +//! Command examples for HeroDB +//! +//! This module contains various example commands and applications +//! that demonstrate how to use HeroDB in different scenarios. + +// Export the example modules +pub mod dbexample_biz; \ No newline at end of file diff --git a/herodb/src/db/base.rs b/herodb/src/db/base.rs index 30da0fd..444cf58 100644 --- a/herodb/src/db/base.rs +++ b/herodb/src/db/base.rs @@ -1,5 +1,6 @@ use bincode; use brotli::{CompressorReader, Decompressor}; +use rhai::CustomType; use serde::{Deserialize, Serialize}; use sled; use std::fmt::Debug; @@ -38,15 +39,11 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized { let mut compressed = Vec::new(); // Default Brotli parameters: quality 5, lgwin 22 (window size) const BROTLI_QUALITY: u32 = 5; - const BROTLI_LGWIN: u32 = 22; + const BROTLI_LGWIN: u32 = 22; const BUFFER_SIZE: usize = 4096; // 4KB buffer - let mut compressor = CompressorReader::new( - &encoded[..], - BUFFER_SIZE, - BROTLI_QUALITY, - BROTLI_LGWIN - ); + let mut compressor = + CompressorReader::new(&encoded[..], BUFFER_SIZE, BROTLI_QUALITY, BROTLI_LGWIN); compressor.read_to_end(&mut compressed)?; Ok(compressed) @@ -56,7 +53,7 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized { fn load_from_bytes(data: &[u8]) -> SledDBResult { let mut decompressed = Vec::new(); const BUFFER_SIZE: usize = 4096; // 4KB buffer - + let mut decompressor = Decompressor::new(data, BUFFER_SIZE); decompressor.read_to_end(&mut decompressed)?; @@ -140,8 +137,8 @@ impl SledDB { Ok(models) } - /// Provides access to the underlying Sled Db instance for advanced operations. - pub fn raw_db(&self) -> &sled::Db { + /// Provides access to the underlying Sled Db instance for advanced operations. + pub fn raw_db(&self) -> &sled::Db { &self.db } } diff --git a/herodb/src/db/db.rs b/herodb/src/db/db.rs index fa124ba..e7bc94b 100644 --- a/herodb/src/db/db.rs +++ b/herodb/src/db/db.rs @@ -1,10 +1,11 @@ use crate::db::base::*; +use bincode; +use rhai::{CustomType, EvalAltResult, TypeBuilder}; use std::any::TypeId; use std::collections::HashMap; +use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; -use std::fmt::Debug; -use bincode; /// Represents a single database operation in a transaction #[derive(Debug, Clone)] @@ -24,7 +25,7 @@ pub trait AnyDbOperations: Send + Sync { fn delete(&self, id: &str) -> SledDBResult<()>; fn get_any(&self, id: &str) -> SledDBResult>; fn list_any(&self) -> SledDBResult>; - fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>; + fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>; fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()>; } @@ -33,17 +34,17 @@ impl AnyDbOperations for SledDB { fn delete(&self, id: &str) -> SledDBResult<()> { self.delete(id) } - + fn get_any(&self, id: &str) -> SledDBResult> { let result = self.get(id)?; Ok(Box::new(result)) } - + fn list_any(&self) -> SledDBResult> { let result = self.list()?; Ok(Box::new(result)) } - + fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> { // Downcast to the specific type T match model.downcast_ref::() { @@ -51,7 +52,7 @@ impl AnyDbOperations for SledDB { None => Err(SledDBError::TypeError), } } - + fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()> { // Deserialize the bytes into model of type T let model: T = bincode::deserialize(serialized)?; @@ -77,23 +78,25 @@ impl TransactionState { } /// Main DB manager that automatically handles all root models +#[derive(Clone, CustomType)] pub struct DB { db_path: PathBuf, - + // Type map for generic operations - type_map: HashMap>, - + type_map: HashMap>, + // Locks to ensure thread safety for key areas _write_locks: Arc>>, - + // Transaction state - transaction: RwLock>, + transaction: Arc>>, } /// Builder for DB that allows registering models +#[derive(Clone, CustomType)] pub struct DBBuilder { base_path: PathBuf, - model_registrations: Vec>, + model_registrations: Vec>, } /// Trait for model registration @@ -129,33 +132,45 @@ impl DBBuilder { model_registrations: Vec::new(), } } - + + pub fn with_path>(base_path: P) -> Self { + Self { + base_path: base_path.into(), + model_registrations: Vec::new(), + } + } + /// Register a model type with the DB pub fn register_model(mut self) -> Self { - self.model_registrations.push(Box::new(SledModelRegistration::::new())); + self.model_registrations + .push(Arc::new(SledModelRegistration::::new())); self } - + /// Build the DB with the registered models - pub fn build(self) -> SledDBResult { + pub fn build(self) -> Result> { let base_path = self.base_path; - + // Ensure base directory exists if !base_path.exists() { - std::fs::create_dir_all(&base_path)?; + std::fs::create_dir_all(&base_path).map_err(|e| { + EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e)) + })?; } - + // Register all models - let mut type_map: HashMap> = HashMap::new(); - + let mut type_map: HashMap> = HashMap::new(); + for registration in self.model_registrations { - let (type_id, db) = registration.register(&base_path)?; - type_map.insert(type_id, db); + let (type_id, db) = registration.register(&base_path).map_err(|e| { + EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e)) + })?; + type_map.insert(type_id, db.into()); } - + let _write_locks = Arc::new(Mutex::new(HashMap::new())); - let transaction = RwLock::new(None); - + let transaction = Arc::new(RwLock::new(None)); + Ok(DB { db_path: base_path, type_map, @@ -169,15 +184,15 @@ impl DB { /// Create a new empty DB instance without any models pub fn new>(base_path: P) -> SledDBResult { let base_path = base_path.into(); - + // Ensure base directory exists if !base_path.exists() { std::fs::create_dir_all(&base_path)?; } - + let _write_locks = Arc::new(Mutex::new(HashMap::new())); - let transaction = RwLock::new(None); - + let transaction = Arc::new(RwLock::new(None)); + Ok(Self { db_path: base_path, type_map: HashMap::new(), @@ -185,25 +200,27 @@ impl DB { transaction, }) } - + // Transaction-related methods - + /// Begin a new transaction pub fn begin_transaction(&self) -> SledDBResult<()> { let mut tx = self.transaction.write().unwrap(); if tx.is_some() { - return Err(SledDBError::GeneralError("Transaction already in progress".into())); + return Err(SledDBError::GeneralError( + "Transaction already in progress".into(), + )); } *tx = Some(TransactionState::new()); Ok(()) } - + /// Check if a transaction is active pub fn has_active_transaction(&self) -> bool { let tx = self.transaction.read().unwrap(); tx.is_some() && tx.as_ref().unwrap().active } - + /// Apply a set operation with the serialized data - bypass transaction check fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> { // Get the database operations for this model type @@ -211,39 +228,47 @@ impl DB { // Just pass the raw serialized data to a special raw insert method return db_ops.insert_any_raw(serialized); } - - Err(SledDBError::GeneralError(format!("No DB registered for type ID {:?}", model_type))) + + Err(SledDBError::GeneralError(format!( + "No DB registered for type ID {:?}", + model_type + ))) } /// Commit the current transaction, applying all operations pub fn commit_transaction(&self) -> SledDBResult<()> { let mut tx_guard = self.transaction.write().unwrap(); - + if let Some(tx_state) = tx_guard.take() { if !tx_state.active { return Err(SledDBError::GeneralError("Transaction not active".into())); } - + // Execute all operations in the transaction for op in tx_state.operations { match op { - DbOperation::Set { model_type, serialized } => { + DbOperation::Set { + model_type, + serialized, + } => { self.apply_set_operation(model_type, &serialized)?; - }, + } DbOperation::Delete { model_type, id } => { - let db_ops = self.type_map.get(&model_type) + let db_ops = self + .type_map + .get(&model_type) .ok_or_else(|| SledDBError::TypeError)?; db_ops.delete(&id)?; } } } - + Ok(()) } else { Err(SledDBError::GeneralError("No active transaction".into())) } } - + /// Rollback the current transaction, discarding all operations pub fn rollback_transaction(&self) -> SledDBResult<()> { let mut tx = self.transaction.write().unwrap(); @@ -253,79 +278,85 @@ impl DB { *tx = None; Ok(()) } - + /// Get the path to the database pub fn path(&self) -> &PathBuf { &self.db_path } - + // Generic methods that work with any supported model type - + /// Insert a model instance into its appropriate database based on type pub fn set(&self, model: &T) -> SledDBResult<()> { // Try to acquire a write lock on the transaction let mut tx_guard = self.transaction.write().unwrap(); - + // Check if there's an active transaction if let Some(tx_state) = tx_guard.as_mut() { if tx_state.active { // Serialize the model for later use let serialized = bincode::serialize(model)?; - + // Record a Set operation in the transaction tx_state.operations.push(DbOperation::Set { model_type: TypeId::of::(), serialized, }); - + return Ok(()); } } - + // If we got here, either there's no transaction or it's not active // Drop the write lock before doing a direct database operation drop(tx_guard); - + // Execute directly match self.type_map.get(&TypeId::of::()) { Some(db_ops) => db_ops.insert_any(model), None => Err(SledDBError::TypeError), } } - + /// Check the transaction state for the given type and id fn check_transaction(&self, id: &str) -> Option, SledDBError>> { // Try to acquire a read lock on the transaction let tx_guard = self.transaction.read().unwrap(); - + if let Some(tx_state) = tx_guard.as_ref() { if !tx_state.active { return None; } - + let type_id = TypeId::of::(); let id_str = id.to_string(); - + // Process operations in reverse order (last operation wins) for op in tx_state.operations.iter().rev() { match op { // First check if this ID has been deleted in the transaction - DbOperation::Delete { model_type, id: op_id } => { + DbOperation::Delete { + model_type, + id: op_id, + } => { if *model_type == type_id && op_id == id { // Return NotFound error for deleted records return Some(Err(SledDBError::NotFound(id.to_string()))); } - }, + } // Then check if it has been set in the transaction - DbOperation::Set { model_type, serialized } => { + DbOperation::Set { + model_type, + serialized, + } => { if *model_type == type_id { // Try to deserialize and check the ID match bincode::deserialize::(serialized) { Ok(model) => { - if model.get_id() == id_str { - return Some(Ok(Some(model))); + if model.get_id() == id_str { + return Some(Ok(Some(model))); + } } - }, Err(_) => continue, // Skip if deserialization fails } } @@ -333,7 +364,7 @@ impl DB { } } } - + // Not found in transaction (continue to database) None } @@ -348,7 +379,7 @@ impl DB { Ok(None) => {} // Should never happen } } - + // If no pending value, look up from the database match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { @@ -358,16 +389,16 @@ impl DB { Ok(t) => Ok(*t), Err(_) => Err(SledDBError::TypeError), } - }, + } None => Err(SledDBError::TypeError), } } - + /// Delete a model instance by its ID and type pub fn delete(&self, id: &str) -> SledDBResult<()> { // Try to acquire a write lock on the transaction let mut tx_guard = self.transaction.write().unwrap(); - + // Check if there's an active transaction if let Some(tx_state) = tx_guard.as_mut() { if tx_state.active { @@ -376,22 +407,22 @@ impl DB { model_type: TypeId::of::(), id: id.to_string(), }); - + return Ok(()); } } - + // If we got here, either there's no transaction or it's not active // Drop the write lock before doing a direct database operation drop(tx_guard); - + // Execute directly match self.type_map.get(&TypeId::of::()) { Some(db_ops) => db_ops.delete(id), None => Err(SledDBError::TypeError), } } - + /// List all model instances of a specific type pub fn list(&self) -> SledDBResult> { // Look up the correct DB operations for type T in our type map @@ -403,24 +434,27 @@ impl DB { Ok(vec_t) => Ok(*vec_t), Err(_) => Err(SledDBError::TypeError), } - }, + } None => Err(SledDBError::TypeError), } } - + // Register a model type with this DB instance pub fn register(&mut self) -> SledDBResult<()> { let db_path = self.db_path.join(T::db_prefix()); let db: SledDB = SledDB::open(db_path)?; - self.type_map.insert(TypeId::of::(), Box::new(db)); + self.type_map.insert(TypeId::of::(), Arc::new(db)); Ok(()) } - + // Get a typed handle to a registered model DB pub fn db_for(&self) -> SledDBResult<&dyn AnyDbOperations> { match self.type_map.get(&TypeId::of::()) { Some(db) => Ok(&**db), - None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::()))), + None => Err(SledDBError::GeneralError(format!( + "No DB registered for type {}", + std::any::type_name::() + ))), } } } diff --git a/herodb/src/db/macros.rs b/herodb/src/db/macros.rs index e77a289..aa784a3 100644 --- a/herodb/src/db/macros.rs +++ b/herodb/src/db/macros.rs @@ -5,25 +5,27 @@ macro_rules! impl_model_methods { impl DB { paste::paste! { /// Insert a model instance into the database - pub fn [](&self, item: &$model) -> SledDBResult<()> { - self.set(item) + pub fn [](&mut self, item: $model) -> Result<(), Box> { + Ok(self.set(&item).map_err(|e| { + rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e)) + })?) } /// Get a model instance by its ID - pub fn [](&self, id: u32) -> SledDBResult<$model> { + pub fn [](&mut self, id: i64) -> SledDBResult<$model> { self.get::<$model>(&id.to_string()) } /// Delete a model instance by its ID - pub fn [](&self, id: u32) -> SledDBResult<()> { + pub fn [](&mut self, id: i64) -> SledDBResult<()> { self.delete::<$model>(&id.to_string()) } /// List all model instances - pub fn [](&self) -> SledDBResult> { + pub fn [](&mut self) -> SledDBResult> { self.list::<$model>() } } } }; -} \ No newline at end of file +} diff --git a/herodb/src/lib.rs b/herodb/src/lib.rs index f1f91be..b018f41 100644 --- a/herodb/src/lib.rs +++ b/herodb/src/lib.rs @@ -7,6 +7,9 @@ pub mod db; pub mod error; pub mod models; +// Temporarily commented out due to compilation errors +// pub mod rhaiengine; +pub mod cmd; // Re-exports pub use error::Error; diff --git a/herodb/src/models/biz/README.md b/herodb/src/models/biz/README.md index 4aa684c..b6a9859 100644 --- a/herodb/src/models/biz/README.md +++ b/herodb/src/models/biz/README.md @@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization ## Model Relationships ``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Currency │◄────┤ Product │◄────┤ SaleItem │ -└─────────────┘ └─────────────┘ └──────┬──────┘ - ▲ │ - │ │ - ┌─────┴──────────┐ │ - │ProductComponent│ │ - └────────────────┘ │ - ▼ ┌─────────────┐ - │ Sale │ - └─────────────┘ + │ Customer │ + └──────┬──────┘ + │ + ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │ +└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘ + ▲ │ + │ │ + ┌─────┴──────────┐ │ + │ProductComponent│ │ + └────────────────┘ │ + │ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + │ + ▼ + ┌─────────────┐ ┌─────────────┐ + │ InvoiceItem │◄────┤ Invoice │ + └─────────────┘ └─────────────┘ ``` +## Business Logic Relationships + +- **Customer**: The entity purchasing products or services +- **Product/Service**: Defines what is being sold, including its base price + - Can be marked as a template (`is_template=true`) to create copies for actual sales +- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations + - Can be linked to a Service when the sale creates an ongoing service +- **Service**: Represents an ongoing service provided to a customer + - Created from a Product template when the product type is Service +- **Invoice**: Represents the billing document for a sale, with payment tracking + - Created from a Sale object to handle billing and payment tracking + ## Root Objects -- root objects are the one who are stored in the DB -- Root Objects are - - currency - - product +- Root objects are the ones stored directly in the DB +- Root Objects are: + - Customer + - Currency + - Product - Sale + - Service + - Invoice ## Models @@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code. **Builder:** - `CurrencyBuilder` - Provides a fluent interface for creating Currency instances +### Customer (Root Object) + +Represents a customer who can purchase products or services. + +**Properties:** +- `id`: u32 - Unique identifier +- `name`: String - Customer name +- `description`: String - Customer description +- `pubkey`: String - Customer's public key +- `contact_ids`: Vec - List of contact IDs +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - Last update timestamp + +**Methods:** +- `add_contact()` - Adds a contact ID to the customer +- `remove_contact()` - Removes a contact ID from the customer + ### Product #### ProductType Enum @@ -70,11 +113,11 @@ Represents a component part of a product. **Builder:** - `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances -#### Product (Root Object) +#### Product (Root Object) Represents a product or service offered. **Properties:** -- `id`: u32 - Unique identifier +- `id`: i64 - Unique identifier - `name`: String - Product name - `description`: String - Product description - `price`: Currency - Product price @@ -83,10 +126,11 @@ Represents a product or service offered. - `status`: ProductStatus - Available or Unavailable - `created_at`: DateTime - Creation timestamp - `updated_at`: DateTime - Last update timestamp -- `max_amount`: u16 - Maximum quantity available +- `max_amount`: i64 - Maximum quantity available - `purchase_till`: DateTime - Deadline for purchasing - `active_till`: DateTime - When product/service expires - `components`: Vec - List of product components +- `is_template`: bool - Whether this is a template product (to be added) **Methods:** - `add_component()` - Adds a component to this product @@ -104,7 +148,62 @@ Represents a product or service offered. - `get_id()` - Returns the ID as a string - `db_prefix()` - Returns "product" as the database prefix -### Sale +### Service (Root Object) + +#### BillingFrequency Enum +Defines how often a service is billed: +- `Hourly` - Billed by the hour +- `Daily` - Billed daily +- `Weekly` - Billed weekly +- `Monthly` - Billed monthly +- `Yearly` - Billed yearly + +#### ServiceStatus Enum +Tracks the status of a service: +- `Active` - Service is currently active +- `Paused` - Service is temporarily paused +- `Cancelled` - Service has been cancelled +- `Completed` - Service has been completed + +#### ServiceItem +Represents an item within a service. + +**Properties:** +- `id`: u32 - Unique identifier +- `service_id`: u32 - Parent service ID +- `product_id`: u32 - ID of the product this service is based on +- `name`: String - Service name +- `description`: String - Detailed description of the service item +- `comments`: String - Additional notes or comments about the service item +- `quantity`: i32 - Number of units +- `unit_price`: Currency - Price per unit +- `subtotal`: Currency - Total price before tax +- `tax_rate`: f64 - Tax rate as a percentage +- `tax_amount`: Currency - Calculated tax amount +- `is_taxable`: bool - Whether this item is taxable +- `active_till`: DateTime - When service expires + +#### Service +Represents an ongoing service provided to a customer. + +**Properties:** +- `id`: u32 - Unique identifier +- `customer_id`: u32 - ID of the customer receiving the service +- `total_amount`: Currency - Total service amount including tax +- `status`: ServiceStatus - Current service status +- `billing_frequency`: BillingFrequency - How often the service is billed +- `service_date`: DateTime - When service started +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - Last update timestamp +- `items`: Vec - List of items in the service +- `is_template`: bool - Whether this is a template service (to be added) + +**Methods:** +- `add_item()` - Adds an item to the service and updates total +- `calculate_total()` - Recalculates the total amount +- `update_status()` - Updates the status of the service + +### Sale #### SaleStatus Enum Tracks the status of a sale: @@ -120,11 +219,18 @@ Represents an item within a sale. - `sale_id`: u32 - Parent sale ID - `product_id`: u32 - ID of the product sold - `name`: String - Product name at time of sale +- `description`: String - Detailed description of the item +- `comments`: String - Additional notes or comments about the item - `quantity`: i32 - Number of items purchased - `unit_price`: Currency - Price per unit -- `subtotal`: Currency - Total price for this item (calculated) +- `subtotal`: Currency - Total price for this item before tax (calculated) +- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%) +- `tax_amount`: Currency - Calculated tax amount for this item - `active_till`: DateTime - When item/service expires +**Methods:** +- `total_with_tax()` - Returns the total amount including tax + **Builder:** - `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances @@ -134,18 +240,24 @@ Represents a complete sale transaction. **Properties:** - `id`: u32 - Unique identifier - `company_id`: u32 - ID of the company making the sale +- `customer_id`: u32 - ID of the customer making the purchase (to be added) - `buyer_name`: String - Name of the buyer - `buyer_email`: String - Email of the buyer -- `total_amount`: Currency - Total sale amount +- `subtotal_amount`: Currency - Total sale amount before tax +- `tax_amount`: Currency - Total tax amount for the sale +- `total_amount`: Currency - Total sale amount including tax - `status`: SaleStatus - Current sale status +- `service_id`: Option - ID of the service created from this sale (to be added) - `sale_date`: DateTime - When sale occurred - `created_at`: DateTime - Creation timestamp - `updated_at`: DateTime - Last update timestamp - `items`: Vec - List of items in the sale **Methods:** -- `add_item()` - Adds an item to the sale and updates total +- `add_item()` - Adds an item to the sale and updates totals - `update_status()` - Updates the status of the sale +- `recalculate_totals()` - Recalculates all totals based on items +- `create_service()` - Creates a service from this sale (to be added) **Builder:** - `SaleBuilder` - Provides a fluent interface for creating Sale instances @@ -223,6 +335,7 @@ let item = SaleItemBuilder::new() .name("Premium Service") .quantity(1) .unit_price(unit_price) + .tax_rate(20.0) // 20% tax rate .active_till(now + Duration::days(30)) .build() .expect("Failed to build sale item"); @@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new() // Update the sale status sale.update_status(SaleStatus::Completed); + +// The sale now contains: +// - subtotal_amount: The sum of all items before tax +// - tax_amount: The sum of all tax amounts +// - total_amount: The total including tax +``` + +### Relationship Between Sale and Invoice + +The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale. + +An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed. + +```rust +// Create an invoice item linked to a sale +let invoice_item = InvoiceItemBuilder::new() + .id(1) + .invoice_id(1) + .description("Premium Service") + .amount(sale.total_amount.clone()) // Use the total amount from the sale + .sale_id(sale.id) // Link to the sale + .build() + .expect("Failed to build invoice item"); ``` ## Database Operations @@ -266,4 +402,125 @@ These methods are available for all root objects: - `insert_product`, `get_product`, `delete_product`, `list_products` for Product - `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency - `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale +- `insert_service`, `get_service`, `delete_service`, `list_services` for Service +- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice +- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer +### Invoice (Root Object) + +#### InvoiceStatus Enum +Tracks the status of an invoice: +- `Draft` - Invoice is in draft state +- `Sent` - Invoice has been sent to the customer +- `Paid` - Invoice has been paid +- `Overdue` - Invoice is past due date +- `Cancelled` - Invoice has been cancelled + +#### PaymentStatus Enum +Tracks the payment status of an invoice: +- `Unpaid` - Invoice has not been paid +- `PartiallyPaid` - Invoice has been partially paid +- `Paid` - Invoice has been fully paid + +#### Payment +Represents a payment made against an invoice. + +**Properties:** +- `amount`: Currency - Payment amount +- `date`: DateTime - Payment date +- `method`: String - Payment method +- `comment`: String - Payment comment + +#### InvoiceItem +Represents an item in an invoice. + +**Properties:** +- `id`: u32 - Unique identifier +- `invoice_id`: u32 - Parent invoice ID +- `description`: String - Item description +- `amount`: Currency - Item amount +- `service_id`: Option - ID of the service this item is for +- `sale_id`: Option - ID of the sale this item is for + +**Methods:** +- `link_to_service()` - Links the invoice item to a service +- `link_to_sale()` - Links the invoice item to a sale + +#### Invoice +Represents an invoice sent to a customer. + +**Properties:** +- `id`: u32 - Unique identifier +- `customer_id`: u32 - ID of the customer being invoiced +- `total_amount`: Currency - Total invoice amount +- `balance_due`: Currency - Amount still due +- `status`: InvoiceStatus - Current invoice status +- `payment_status`: PaymentStatus - Current payment status +- `issue_date`: DateTime - When invoice was issued +- `due_date`: DateTime - When payment is due +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - Last update timestamp +- `items`: Vec - List of items in the invoice +- `payments`: Vec - List of payments made + +**Methods:** +- `add_item()` - Adds an item to the invoice +- `calculate_total()` - Calculates the total amount +- `add_payment()` - Adds a payment to the invoice +- `calculate_balance()` - Calculates the balance due +- `update_payment_status()` - Updates the payment status +- `update_status()` - Updates the status of the invoice +- `is_overdue()` - Checks if the invoice is overdue +- `check_if_overdue()` - Marks the invoice as overdue if past due date + +### Relationships Between Models + +#### Product/Service Templates and Instances + +Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold. + +#### Sale to Service Relationship + +When a product of type `Service` is sold, a Service instance can be created from the Sale: + +```rust +// Create a service from a sale +let service = sale.create_service( + service_id, + ServiceStatus::Active, + BillingFrequency::Monthly +); +``` + +#### Sale to Invoice Relationship + +An Invoice is created from a Sale to handle billing and payment tracking: + +```rust +// Create an invoice from a sale +let invoice = Invoice::from_sale( + invoice_id, + sale, + due_date +); +``` + +#### Customer-Centric View + +The models allow tracking all customer interactions: + +- What products/services they've purchased (via Sale records) +- What ongoing services they have (via Service records) +- What they've been invoiced for (via Invoice records) +- What they've paid (via Payment records in Invoices) + +```rust +// Get all sales for a customer +let customer_sales = db.list_sales_by_customer(customer_id); + +// Get all services for a customer +let customer_services = db.list_services_by_customer(customer_id); + +// Get all invoices for a customer +let customer_invoices = db.list_invoices_by_customer(customer_id); +``` diff --git a/herodb/src/models/biz/business_models_plan.md b/herodb/src/models/biz/business_models_plan.md deleted file mode 100644 index d68ec76..0000000 --- a/herodb/src/models/biz/business_models_plan.md +++ /dev/null @@ -1,371 +0,0 @@ -# Business Models Implementation Plan - -## Overview - -This document outlines the plan for implementing new business models in the codebase: - -1. **Service**: For tracking recurring payments (similar to Sale) -2. **Customer**: For storing customer information -3. **Contract**: For linking services or sales to customers -4. **Invoice**: For invoicing customers - -## Model Diagrams - -### Core Models and Relationships - -```mermaid -classDiagram - class Service { - +id: u32 - +customer_id: u32 - +total_amount: Currency - +status: ServiceStatus - +billing_frequency: BillingFrequency - +service_date: DateTime~Utc~ - +created_at: DateTime~Utc~ - +updated_at: DateTime~Utc~ - +items: Vec~ServiceItem~ - +calculate_total() - } - - class ServiceItem { - +id: u32 - +service_id: u32 - +name: String - +quantity: i32 - +unit_price: Currency - +subtotal: Currency - +tax_rate: f64 - +tax_amount: Currency - +is_taxable: bool - +active_till: DateTime~Utc~ - } - - class Customer { - +id: u32 - +name: String - +description: String - +pubkey: String - +contact_ids: Vec~u32~ - +created_at: DateTime~Utc~ - +updated_at: DateTime~Utc~ - } - - class Contract { - +id: u32 - +customer_id: u32 - +service_id: Option~u32~ - +sale_id: Option~u32~ - +terms: String - +start_date: DateTime~Utc~ - +end_date: DateTime~Utc~ - +auto_renewal: bool - +renewal_terms: String - +status: ContractStatus - +created_at: DateTime~Utc~ - +updated_at: DateTime~Utc~ - } - - class Invoice { - +id: u32 - +customer_id: u32 - +total_amount: Currency - +balance_due: Currency - +status: InvoiceStatus - +payment_status: PaymentStatus - +issue_date: DateTime~Utc~ - +due_date: DateTime~Utc~ - +created_at: DateTime~Utc~ - +updated_at: DateTime~Utc~ - +items: Vec~InvoiceItem~ - +payments: Vec~Payment~ - } - - class InvoiceItem { - +id: u32 - +invoice_id: u32 - +description: String - +amount: Currency - +service_id: Option~u32~ - +sale_id: Option~u32~ - } - - class Payment { - +amount: Currency - +date: DateTime~Utc~ - +method: String - } - - Service "1" -- "many" ServiceItem : contains - Customer "1" -- "many" Service : has - Customer "1" -- "many" Contract : has - Contract "1" -- "0..1" Service : references - Contract "1" -- "0..1" Sale : references - Invoice "1" -- "many" InvoiceItem : contains - Invoice "1" -- "many" Payment : contains - Customer "1" -- "many" Invoice : has - InvoiceItem "1" -- "0..1" Service : references - InvoiceItem "1" -- "0..1" Sale : references -``` - -### Enums and Supporting Types - -```mermaid -classDiagram - class BillingFrequency { - <> - Hourly - Daily - Weekly - Monthly - Yearly - } - - class ServiceStatus { - <> - Active - Paused - Cancelled - Completed - } - - class ContractStatus { - <> - Active - Expired - Terminated - } - - class InvoiceStatus { - <> - Draft - Sent - Paid - Overdue - Cancelled - } - - class PaymentStatus { - <> - Unpaid - PartiallyPaid - Paid - } - - Service -- ServiceStatus : has - Service -- BillingFrequency : has - Contract -- ContractStatus : has - Invoice -- InvoiceStatus : has - Invoice -- PaymentStatus : has -``` - -## Detailed Implementation Plan - -### 1. Service and ServiceItem (service.rs) - -The Service model will be similar to Sale but designed for recurring payments: - -- **Service**: Main struct for tracking recurring services - - Fields: - - id: u32 - - customer_id: u32 - - total_amount: Currency - - status: ServiceStatus - - billing_frequency: BillingFrequency - - service_date: DateTime - - created_at: DateTime - - updated_at: DateTime - - items: Vec - - Methods: - - calculate_total(): Updates the total_amount based on all items - - add_item(item: ServiceItem): Adds an item and updates the total - - update_status(status: ServiceStatus): Updates the status and timestamp - -- **ServiceItem**: Items within a service (similar to SaleItem) - - Fields: - - id: u32 - - service_id: u32 - - name: String - - quantity: i32 - - unit_price: Currency - - subtotal: Currency - - tax_rate: f64 - - tax_amount: Currency - - is_taxable: bool - - active_till: DateTime - - Methods: - - calculate_subtotal(): Calculates subtotal based on quantity and unit_price - - calculate_tax(): Calculates tax amount based on subtotal and tax_rate - -- **BillingFrequency**: Enum for different billing periods - - Variants: Hourly, Daily, Weekly, Monthly, Yearly - -- **ServiceStatus**: Enum for service status - - Variants: Active, Paused, Cancelled, Completed - -### 2. Customer (customer.rs) - -The Customer model will store customer information: - -- **Customer**: Main struct for customer data - - Fields: - - id: u32 - - name: String - - description: String - - pubkey: String - - contact_ids: Vec - - created_at: DateTime - - updated_at: DateTime - - Methods: - - add_contact(contact_id: u32): Adds a contact ID to the list - - remove_contact(contact_id: u32): Removes a contact ID from the list - -### 3. Contract (contract.rs) - -The Contract model will link services or sales to customers: - -- **Contract**: Main struct for contract data - - Fields: - - id: u32 - - customer_id: u32 - - service_id: Option - - sale_id: Option - - terms: String - - start_date: DateTime - - end_date: DateTime - - auto_renewal: bool - - renewal_terms: String - - status: ContractStatus - - created_at: DateTime - - updated_at: DateTime - - Methods: - - is_active(): bool - Checks if the contract is currently active - - is_expired(): bool - Checks if the contract has expired - - renew(): Updates the contract dates based on renewal terms - -- **ContractStatus**: Enum for contract status - - Variants: Active, Expired, Terminated - -### 4. Invoice (invoice.rs) - -The Invoice model will handle billing: - -- **Invoice**: Main struct for invoice data - - Fields: - - id: u32 - - customer_id: u32 - - total_amount: Currency - - balance_due: Currency - - status: InvoiceStatus - - payment_status: PaymentStatus - - issue_date: DateTime - - due_date: DateTime - - created_at: DateTime - - updated_at: DateTime - - items: Vec - - payments: Vec - - Methods: - - calculate_total(): Updates the total_amount based on all items - - add_item(item: InvoiceItem): Adds an item and updates the total - - add_payment(payment: Payment): Adds a payment and updates balance_due and payment_status - - update_status(status: InvoiceStatus): Updates the status and timestamp - - calculate_balance(): Updates the balance_due based on total_amount and payments - -- **InvoiceItem**: Items within an invoice - - Fields: - - id: u32 - - invoice_id: u32 - - description: String - - amount: Currency - - service_id: Option - - sale_id: Option - -- **Payment**: Struct for tracking payments - - Fields: - - amount: Currency - - date: DateTime - - method: String - -- **InvoiceStatus**: Enum for invoice status - - Variants: Draft, Sent, Paid, Overdue, Cancelled - -- **PaymentStatus**: Enum for payment status - - Variants: Unpaid, PartiallyPaid, Paid - -### 5. Updates to mod.rs - -We'll need to update the mod.rs file to include the new modules and re-export the types: - -```rust -pub mod currency; -pub mod product; -pub mod sale; -pub mod exchange_rate; -pub mod service; -pub mod customer; -pub mod contract; -pub mod invoice; - -// 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}; -pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency}; -pub use customer::Customer; -pub use contract::{Contract, ContractStatus}; -pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment}; - -// Re-export builder types -pub use product::{ProductBuilder, ProductComponentBuilder}; -pub use sale::{SaleBuilder, SaleItemBuilder}; -pub use currency::CurrencyBuilder; -pub use exchange_rate::ExchangeRateBuilder; -pub use service::{ServiceBuilder, ServiceItemBuilder}; -pub use customer::CustomerBuilder; -pub use contract::ContractBuilder; -pub use invoice::{InvoiceBuilder, InvoiceItemBuilder}; -``` - -### 6. Updates to model_methods.rs - -We'll need to update the model_methods.rs file to implement the model methods for the new models: - -```rust -use crate::db::db::DB; -use crate::db::base::{SledDBResult, SledModel}; -use crate::impl_model_methods; -use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice}; - -// Implement model-specific methods for Product -impl_model_methods!(Product, product, products); - -// Implement model-specific methods for Sale -impl_model_methods!(Sale, sale, sales); - -// Implement model-specific methods for Currency -impl_model_methods!(Currency, currency, currencies); - -// Implement model-specific methods for ExchangeRate -impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates); - -// Implement model-specific methods for Service -impl_model_methods!(Service, service, services); - -// Implement model-specific methods for Customer -impl_model_methods!(Customer, customer, customers); - -// Implement model-specific methods for Contract -impl_model_methods!(Contract, contract, contracts); - -// Implement model-specific methods for Invoice -impl_model_methods!(Invoice, invoice, invoices); -``` - -## Implementation Approach - -1. Create the new model files (service.rs, customer.rs, contract.rs, invoice.rs) -2. Implement the structs, enums, and methods for each model -3. Update mod.rs to include the new modules and re-export the types -4. Update model_methods.rs to implement the model methods for the new models -5. Test the new models with example code \ No newline at end of file diff --git a/herodb/src/models/biz/currency.rs b/herodb/src/models/biz/currency.rs index cdf385d..ab74617 100644 --- a/herodb/src/models/biz/currency.rs +++ b/herodb/src/models/biz/currency.rs @@ -1,10 +1,10 @@ -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; +use crate::db::base::{SledModel, Storable}; +use chrono::{DateTime, Duration, Utc}; +use rhai::{CustomType, EvalAltResult, TypeBuilder}; +use serde::{Deserialize, Serialize}; // Import Sled traits from db module /// Currency represents a monetary value with amount and currency code -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Currency { pub amount: f64, pub currency_code: String, @@ -19,28 +19,13 @@ impl Currency { } } - /// 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())) + pub fn amount(&mut self) -> f64 { + self.amount } } /// Builder for Currency +#[derive(Clone, CustomType)] pub struct CurrencyBuilder { amount: Option, currency_code: Option, @@ -68,7 +53,7 @@ impl CurrencyBuilder { } /// Build the Currency object - pub fn build(self) -> Result { + pub fn build(self) -> Result> { Ok(Currency { amount: self.amount.ok_or("amount is required")?, currency_code: self.currency_code.ok_or("currency_code is required")?, diff --git a/herodb/src/models/biz/invoice.rs b/herodb/src/models/biz/invoice.rs index e4030c1..5c66257 100644 --- a/herodb/src/models/biz/invoice.rs +++ b/herodb/src/models/biz/invoice.rs @@ -27,15 +27,17 @@ pub struct Payment { pub amount: Currency, pub date: DateTime, pub method: String, + pub comment: String, } impl Payment { /// Create a new payment - pub fn new(amount: Currency, method: String) -> Self { + pub fn new(amount: Currency, method: String, comment: String) -> Self { Self { amount, date: Utc::now(), method, + comment, } } } diff --git a/herodb/src/models/biz/product.rs b/herodb/src/models/biz/product.rs index ec8a76d..bc3d793 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; +use crate::db::base::{SledModel, Storable}; +use chrono::{DateTime, Duration, Utc}; +use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module}; +use serde::{Deserialize, Serialize}; // Import Sled traits from db module /// ProductType represents the type of a product #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -13,10 +13,6 @@ 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, } @@ -24,19 +20,17 @@ pub enum ProductStatus { /// ProductComponent represents a component of a product #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProductComponent { - pub id: u32, + pub id: i64, pub name: String, pub description: String, - pub quantity: i32, + pub quantity: i64, 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 { + pub fn new(id: i64, name: String, description: String, quantity: i64) -> Self { let now = Utc::now(); Self { id, @@ -45,32 +39,19 @@ 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 +#[derive(Clone, CustomType)] pub struct ProductComponentBuilder { - id: Option, + id: Option, name: Option, description: Option, - quantity: Option, + quantity: Option, created_at: Option>, updated_at: Option>, - energy_usage: Option, - cost: Option, } impl ProductComponentBuilder { @@ -83,13 +64,11 @@ impl ProductComponentBuilder { quantity: None, created_at: None, updated_at: None, - energy_usage: None, - cost: None, } } /// Set the id - pub fn id(mut self, id: u32) -> Self { + pub fn id(mut self, id: i64) -> Self { self.id = Some(id); self } @@ -107,7 +86,7 @@ impl ProductComponentBuilder { } /// Set the quantity - pub fn quantity(mut self, quantity: i32) -> Self { + pub fn quantity(mut self, quantity: i64) -> Self { self.quantity = Some(quantity); self } @@ -124,20 +103,8 @@ 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 { + pub fn build(self) -> Result> { let now = Utc::now(); Ok(ProductComponent { id: self.id.ok_or("id is required")?, @@ -146,16 +113,19 @@ 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())), }) } } +<<<<<<< HEAD /// Product represents a product or service offered in the system #[derive(Debug, Clone, Serialize, Deserialize)] +======= +/// Product represents a product or service offered by the Freezone +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +>>>>>>> builders_in_script pub struct Product { - pub id: u32, + pub id: i64, pub name: String, pub description: String, pub price: Currency, @@ -164,7 +134,7 @@ pub struct Product { 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 max_amount: i64, // 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, @@ -175,14 +145,14 @@ pub struct Product { impl Product { /// Create a new product with default timestamps pub fn new( - id: u32, + id: i64, name: String, description: String, price: Currency, type_: ProductType, category: String, status: ProductStatus, - max_amount: u16, + max_amount: i64, validity_days: i64, // How many days the product is valid after purchase ) -> Self { let now = Utc::now(); @@ -203,86 +173,40 @@ impl Product { 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 + self.status == ProductStatus::Available && 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 +#[derive(Clone, CustomType)] pub struct ProductBuilder { - id: Option, + id: Option, name: Option, description: Option, price: Option, @@ -291,7 +215,7 @@ pub struct ProductBuilder { status: Option, created_at: Option>, updated_at: Option>, - max_amount: Option, + max_amount: Option, purchase_till: Option>, active_till: Option>, components: Vec, @@ -320,7 +244,7 @@ impl ProductBuilder { } /// Set the id - pub fn id(mut self, id: u32) -> Self { + pub fn id(mut self, id: i64) -> Self { self.id = Some(id); self } @@ -362,7 +286,7 @@ impl ProductBuilder { } /// Set the max amount - pub fn max_amount(mut self, max_amount: u16) -> Self { + pub fn max_amount(mut self, max_amount: i64) -> Self { self.max_amount = Some(max_amount); self } @@ -396,13 +320,15 @@ impl ProductBuilder { 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)) + 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")? + self.active_till + .ok_or("Either active_till or validity_days must be provided")? }; Ok(Product { @@ -438,4 +364,4 @@ impl SledModel for Product { } // Import Currency from the currency module -use crate::models::biz::Currency; \ No newline at end of file +use crate::models::biz::Currency; diff --git a/herodb/src/models/biz/sale.rs b/herodb/src/models/biz/sale.rs index bcfede1..00f7c43 100644 --- a/herodb/src/models/biz/sale.rs +++ b/herodb/src/models/biz/sale.rs @@ -1,7 +1,8 @@ -use crate::models::biz::Currency; // Use crate:: for importing from the module -use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use crate::db::base::{SledModel, Storable}; +use crate::models::biz::Currency; // Use crate:: for importing from the module // Import Sled traits from db module // use super::db::Model; // Removed old Model trait import use chrono::{DateTime, Utc}; +use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; // use std::collections::HashMap; // Removed unused import @@ -20,9 +21,13 @@ pub struct SaleItem { pub sale_id: u32, pub product_id: u32, pub name: String, + pub description: String, // Description of the item + pub comments: String, // Additional comments about the item pub quantity: i32, pub unit_price: Currency, pub subtotal: Currency, + pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%) + pub tax_amount: Currency, // Calculated tax amount pub active_till: DateTime, // after this product no longer active if e.g. a service } @@ -33,39 +38,66 @@ impl SaleItem { sale_id: u32, product_id: u32, name: String, + description: String, + comments: String, quantity: i32, unit_price: Currency, + tax_rate: f64, active_till: DateTime, ) -> Self { - // Calculate subtotal + // Calculate subtotal (before tax) let amount = unit_price.amount * quantity as f64; let subtotal = Currency { amount, currency_code: unit_price.currency_code.clone(), }; + // Calculate tax amount + let tax_amount_value = subtotal.amount * (tax_rate / 100.0); + let tax_amount = Currency { + amount: tax_amount_value, + currency_code: unit_price.currency_code.clone(), + }; + Self { id, sale_id, product_id, name, + description, + comments, quantity, unit_price, subtotal, + tax_rate, + tax_amount, active_till, } } + + /// Get the total amount including tax + pub fn total_with_tax(&self) -> Currency { + Currency { + amount: self.subtotal.amount + self.tax_amount.amount, + currency_code: self.subtotal.currency_code.clone(), + } + } } /// Builder for SaleItem +#[derive(Clone, CustomType)] pub struct SaleItemBuilder { id: Option, sale_id: Option, product_id: Option, name: Option, + description: Option, + comments: Option, quantity: Option, unit_price: Option, subtotal: Option, + tax_rate: Option, + tax_amount: Option, active_till: Option>, } @@ -77,9 +109,13 @@ impl SaleItemBuilder { sale_id: None, product_id: None, name: None, + description: None, + comments: None, quantity: None, unit_price: None, subtotal: None, + tax_rate: None, + tax_amount: None, active_till: None, } } @@ -107,6 +143,18 @@ impl SaleItemBuilder { 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 comments + pub fn comments>(mut self, comments: S) -> Self { + self.comments = Some(comments.into()); + self + } /// Set the quantity pub fn quantity(mut self, quantity: i32) -> Self { @@ -120,6 +168,12 @@ impl SaleItemBuilder { self } + /// Set the tax_rate + pub fn tax_rate(mut self, tax_rate: f64) -> Self { + self.tax_rate = Some(tax_rate); + self + } + /// Set the active_till pub fn active_till(mut self, active_till: DateTime) -> Self { self.active_till = Some(active_till); @@ -130,36 +184,52 @@ impl SaleItemBuilder { pub fn build(self) -> Result { let unit_price = self.unit_price.ok_or("unit_price is required")?; let quantity = self.quantity.ok_or("quantity is required")?; - + let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified + // Calculate subtotal let amount = unit_price.amount * quantity as f64; let subtotal = Currency { amount, currency_code: unit_price.currency_code.clone(), }; + + // Calculate tax amount + let tax_amount_value = subtotal.amount * (tax_rate / 100.0); + let tax_amount = Currency { + amount: tax_amount_value, + currency_code: unit_price.currency_code.clone(), + }; Ok(SaleItem { id: self.id.ok_or("id is required")?, sale_id: self.sale_id.ok_or("sale_id is required")?, product_id: self.product_id.ok_or("product_id is required")?, name: self.name.ok_or("name is required")?, + description: self.description.unwrap_or_default(), + comments: self.comments.unwrap_or_default(), quantity, unit_price, subtotal, + tax_rate, + tax_amount, active_till: self.active_till.ok_or("active_till is required")?, }) } } /// Sale represents a sale of products or services -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Sale { pub id: u32, pub company_id: u32, + pub customer_id: u32, // ID of the customer making the purchase pub buyer_name: String, pub buyer_email: String, - pub total_amount: Currency, + pub subtotal_amount: Currency, // Total before tax + pub tax_amount: Currency, // Total tax + pub total_amount: Currency, // Total including tax pub status: SaleStatus, + pub service_id: Option, // ID of the service created from this sale (if applicable) pub sale_date: DateTime, pub created_at: DateTime, pub updated_at: DateTime, @@ -173,66 +243,172 @@ impl Sale { pub fn new( id: u32, company_id: u32, + customer_id: u32, buyer_name: String, buyer_email: String, currency_code: String, status: SaleStatus, ) -> Self { let now = Utc::now(); + let zero_currency = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; + Self { id, company_id, + customer_id, buyer_name, buyer_email, - total_amount: Currency { amount: 0.0, currency_code }, + subtotal_amount: zero_currency.clone(), + tax_amount: zero_currency.clone(), + total_amount: zero_currency, status, + service_id: None, sale_date: now, created_at: now, updated_at: now, items: Vec::new(), } } - + /// Add an item to the sale and update the total amount pub fn add_item(&mut self, item: SaleItem) { // Make sure the item's sale_id matches this sale assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id"); - - // Update the total amount + + // Update the amounts if self.items.is_empty() { - // First item, initialize the total amount with the same currency - self.total_amount = Currency { + // First item, initialize the amounts with the same currency + self.subtotal_amount = Currency { amount: item.subtotal.amount, currency_code: item.subtotal.currency_code.clone(), }; + self.tax_amount = Currency { + amount: item.tax_amount.amount, + currency_code: item.tax_amount.currency_code.clone(), + }; + self.total_amount = Currency { + amount: item.subtotal.amount + item.tax_amount.amount, + currency_code: item.subtotal.currency_code.clone(), + }; } else { - // Add to the existing total + // Add to the existing totals // (Assumes all items have the same currency) - self.total_amount.amount += item.subtotal.amount; + self.subtotal_amount.amount += item.subtotal.amount; + self.tax_amount.amount += item.tax_amount.amount; + self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount; } - + // Add the item to the list self.items.push(item); - + // Update the sale timestamp self.updated_at = Utc::now(); } + /// Recalculate all totals based on items + pub fn recalculate_totals(&mut self) { + if self.items.is_empty() { + return; + } + + // Get the currency code from the first item + let currency_code = self.items[0].subtotal.currency_code.clone(); + + // Calculate the totals + let mut subtotal = 0.0; + let mut tax_total = 0.0; + + for item in &self.items { + subtotal += item.subtotal.amount; + tax_total += item.tax_amount.amount; + } + + // Update the amounts + self.subtotal_amount = Currency { + amount: subtotal, + currency_code: currency_code.clone(), + }; + self.tax_amount = Currency { + amount: tax_total, + currency_code: currency_code.clone(), + }; + self.total_amount = Currency { + amount: subtotal + tax_total, + currency_code, + }; + + // Update the timestamp + self.updated_at = Utc::now(); + } + /// Update the status of the sale pub fn update_status(&mut self, status: SaleStatus) { self.status = status; self.updated_at = Utc::now(); } + + /// Create a service from this sale + /// This method should be called when a product of type Service is sold + pub fn create_service(&mut self, service_id: u32, status: crate::models::biz::ServiceStatus, billing_frequency: crate::models::biz::BillingFrequency) -> Result { + use crate::models::biz::{Service, ServiceItem, ServiceStatus, BillingFrequency}; + + // Create a new service + let mut service = Service::new( + service_id, + self.customer_id, + self.total_amount.currency_code.clone(), + status, + billing_frequency, + ); + + // Convert sale items to service items + for sale_item in &self.items { + // Check if the product is a service type + // In a real implementation, you would check the product type from the database + + // Create a service item from the sale item + let service_item = ServiceItem::new( + sale_item.id, + service_id, + sale_item.product_id, + sale_item.name.clone(), + sale_item.description.clone(), // Copy description from sale item + sale_item.comments.clone(), // Copy comments from sale item + sale_item.quantity, + sale_item.unit_price.clone(), + sale_item.tax_rate, + true, // is_taxable + sale_item.active_till, + ); + + // Add the service item to the service + service.add_item(service_item); + } + + // Link this sale to the service + self.service_id = Some(service_id); + self.updated_at = Utc::now(); + + Ok(service) + } } /// Builder for Sale +#[derive(Clone, CustomType)] pub struct SaleBuilder { id: Option, company_id: Option, + customer_id: Option, buyer_name: Option, buyer_email: Option, + subtotal_amount: Option, + tax_amount: Option, total_amount: Option, status: Option, + service_id: Option, sale_date: Option>, created_at: Option>, updated_at: Option>, @@ -246,10 +422,14 @@ impl SaleBuilder { Self { id: None, company_id: None, + customer_id: None, buyer_name: None, buyer_email: None, + subtotal_amount: None, + tax_amount: None, total_amount: None, status: None, + service_id: None, sale_date: None, created_at: None, updated_at: None, @@ -269,6 +449,12 @@ impl SaleBuilder { self.company_id = Some(company_id); self } + + /// Set the customer_id + pub fn customer_id(mut self, customer_id: u32) -> Self { + self.customer_id = Some(customer_id); + self + } /// Set the buyer_name pub fn buyer_name>(mut self, buyer_name: S) -> Self { @@ -293,6 +479,12 @@ impl SaleBuilder { self.status = Some(status); self } + + /// Set the service_id + pub fn service_id(mut self, service_id: u32) -> Self { + self.service_id = Some(service_id); + self + } /// Set the sale_date pub fn sale_date(mut self, sale_date: DateTime) -> Self { @@ -311,40 +503,46 @@ impl SaleBuilder { let now = Utc::now(); let id = self.id.ok_or("id is required")?; let currency_code = self.currency_code.ok_or("currency_code is required")?; - - // Initialize with empty total amount + + // Initialize with empty amounts + let mut subtotal_amount = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; + let mut tax_amount = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; let mut total_amount = Currency { amount: 0.0, currency_code: currency_code.clone(), }; - - // Calculate total amount from items + + // Calculate amounts from items for item in &self.items { // Make sure the item's sale_id matches this sale if item.sale_id != id { return Err("Item sale_id must match sale id"); } - - if total_amount.amount == 0.0 { - // First item, initialize the total amount with the same currency - total_amount = Currency { - amount: item.subtotal.amount, - currency_code: item.subtotal.currency_code.clone(), - }; - } else { - // Add to the existing total - // (Assumes all items have the same currency) - total_amount.amount += item.subtotal.amount; - } + + subtotal_amount.amount += item.subtotal.amount; + tax_amount.amount += item.tax_amount.amount; } + + // Calculate total amount + total_amount.amount = subtotal_amount.amount + tax_amount.amount; Ok(Sale { id, company_id: self.company_id.ok_or("company_id is required")?, + customer_id: self.customer_id.ok_or("customer_id is required")?, buyer_name: self.buyer_name.ok_or("buyer_name is required")?, buyer_email: self.buyer_email.ok_or("buyer_email is required")?, + subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount), + tax_amount: self.tax_amount.unwrap_or(tax_amount), total_amount: self.total_amount.unwrap_or(total_amount), status: self.status.ok_or("status is required")?, + service_id: self.service_id, sale_date: self.sale_date.unwrap_or(now), created_at: self.created_at.unwrap_or(now), updated_at: self.updated_at.unwrap_or(now), diff --git a/herodb/src/models/biz/service.rs b/herodb/src/models/biz/service.rs index 2c1eb05..c22b57f 100644 --- a/herodb/src/models/biz/service.rs +++ b/herodb/src/models/biz/service.rs @@ -29,6 +29,8 @@ pub struct ServiceItem { pub service_id: u32, pub product_id: u32, pub name: String, + pub description: String, // Description of the service item + pub comments: String, // Additional comments about the service item pub quantity: i32, pub unit_price: Currency, pub subtotal: Currency, @@ -45,6 +47,8 @@ impl ServiceItem { service_id: u32, product_id: u32, name: String, + description: String, + comments: String, quantity: i32, unit_price: Currency, tax_rate: f64, @@ -76,6 +80,8 @@ impl ServiceItem { service_id, product_id, name, + description, + comments, quantity, unit_price, subtotal, @@ -117,6 +123,8 @@ pub struct ServiceItemBuilder { service_id: Option, product_id: Option, name: Option, + description: Option, + comments: Option, quantity: Option, unit_price: Option, subtotal: Option, @@ -134,6 +142,8 @@ impl ServiceItemBuilder { service_id: None, product_id: None, name: None, + description: None, + comments: None, quantity: None, unit_price: None, subtotal: None, @@ -167,6 +177,18 @@ impl ServiceItemBuilder { 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 comments + pub fn comments>(mut self, comments: S) -> Self { + self.comments = Some(comments.into()); + self + } /// Set the quantity pub fn quantity(mut self, quantity: i32) -> Self { @@ -230,6 +252,8 @@ impl ServiceItemBuilder { service_id: self.service_id.ok_or("service_id is required")?, product_id: self.product_id.ok_or("product_id is required")?, name: self.name.ok_or("name is required")?, + description: self.description.unwrap_or_default(), + comments: self.comments.unwrap_or_default(), quantity, unit_price, subtotal, diff --git a/herodb/src/models/circle/circle.rs b/herodb/src/models/circle/circle.rs index a8d521d..62e1995 100644 --- a/herodb/src/models/circle/circle.rs +++ b/herodb/src/models/circle/circle.rs @@ -2,33 +2,12 @@ use serde::{Deserialize, Serialize}; use crate::db::{SledModel, Storable}; use std::collections::HashMap; -/// Role represents the role of a member in a circle -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Role { - Admin, - Stakeholder, - Member, - Contributor, - Guest, -} - -/// Member represents a member of a circle -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Member { - pub pubkeys: Vec, // public keys of the member - pub emails: Vec, // list of emails - pub name: String, // name of the member - pub description: String, // optional description - pub role: Role, // role of the member in the circle -} - /// Circle represents a collection of members (users or other circles) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Circle { pub id: u32, // unique id pub name: String, // name of the circle pub description: String, // optional description - pub members: Vec, // members of the circle } impl Circle { @@ -38,15 +17,9 @@ impl Circle { id, name, description, - members: Vec::new(), } } - /// Add a member to the circle - pub fn add_member(&mut self, member: Member) { - self.members.push(member); - } - /// Returns a map of index keys for this circle pub fn index_keys(&self) -> HashMap { let mut keys = HashMap::new(); diff --git a/herodb/src/models/circle/member.rs b/herodb/src/models/circle/member.rs new file mode 100644 index 0000000..c39fa1f --- /dev/null +++ b/herodb/src/models/circle/member.rs @@ -0,0 +1,82 @@ +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable}; +use std::collections::HashMap; + +/// Role represents the role of a member in a circle +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Role { + Admin, + Stakeholder, + Member, + Contributor, + Guest, +} + +/// Member represents a member of a circle +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Member { + pub id: u32, // unique id + pub emails: Vec, // list of emails + pub name: String, // name of the member + pub description: String, // optional description + pub role: Role, // role of the member in the circle + pub contact_ids: Vec, // IDs of contacts linked to this member + pub wallet_ids: Vec, // IDs of wallets owned by this member +} + +impl Member { + /// Create a new member + pub fn new(id: u32, name: String, description: String, role: Role) -> Self { + Self { + id, + emails: Vec::new(), + name, + description, + role, + contact_ids: Vec::new(), + wallet_ids: Vec::new(), + } + } + + /// Add an email to this member + pub fn add_email(&mut self, email: String) { + if !self.emails.contains(&email) { + self.emails.push(email); + } + } + + /// Link a contact to this member + pub fn link_contact(&mut self, contact_id: u32) { + if !self.contact_ids.contains(&contact_id) { + self.contact_ids.push(contact_id); + } + } + + /// Link a wallet to this member + pub fn link_wallet(&mut self, wallet_id: u32) { + if !self.wallet_ids.contains(&wallet_id) { + self.wallet_ids.push(wallet_id); + } + } + + /// Returns a map of index keys for this member + pub fn index_keys(&self) -> HashMap { + let mut keys = HashMap::new(); + keys.insert("name".to_string(), self.name.clone()); + keys + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Member {} + +// Implement SledModel trait +impl SledModel for Member { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "member" + } +} \ No newline at end of file diff --git a/herodb/src/models/circle/mod.rs b/herodb/src/models/circle/mod.rs index b47d562..2f578c4 100644 --- a/herodb/src/models/circle/mod.rs +++ b/herodb/src/models/circle/mod.rs @@ -1,9 +1,13 @@ pub mod circle; +pub mod member; pub mod name; +pub mod wallet; // Re-export all model types for convenience -pub use circle::{Circle, Member, Role}; +pub use circle::Circle; +pub use member::{Member, Role}; pub use name::{Name, Record, RecordType}; +pub use wallet::{Wallet, Asset}; // Re-export database components from db module pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; diff --git a/herodb/src/models/circle/wallet.rs b/herodb/src/models/circle/wallet.rs new file mode 100644 index 0000000..ff4b5e1 --- /dev/null +++ b/herodb/src/models/circle/wallet.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable}; +use std::collections::HashMap; + +/// Asset represents a cryptocurrency asset in a wallet +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Asset { + pub name: String, // Asset name (e.g., "USDC") + pub amount: f64, // Amount of the asset +} + +impl Asset { + /// Create a new asset + pub fn new(name: String, amount: f64) -> Self { + Self { + name, + amount, + } + } +} + +/// Wallet represents a cryptocurrency wallet +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Wallet { + pub id: u32, // unique id + pub name: String, // name of the wallet + pub description: String, // optional description + pub blockchain_name: String, // name of the blockchain + pub pubkey: String, // public key of the wallet + pub assets: Vec, // assets in the wallet +} + +impl Wallet { + /// Create a new wallet + pub fn new(id: u32, name: String, description: String, blockchain_name: String, pubkey: String) -> Self { + Self { + id, + name, + description, + blockchain_name, + pubkey, + assets: Vec::new(), + } + } + + /// Set an asset in the wallet (replaces if exists, adds if not) + pub fn set_asset(&mut self, name: String, amount: f64) { + // Check if the asset already exists + if let Some(asset) = self.assets.iter_mut().find(|a| a.name == name) { + // Update the amount + asset.amount = amount; + } else { + // Add a new asset + self.assets.push(Asset::new(name, amount)); + } + } + + /// Get the total value of all assets in the wallet + pub fn total_value(&self) -> f64 { + self.assets.iter().map(|a| a.amount).sum() + } + + /// Returns a map of index keys for this wallet + pub fn index_keys(&self) -> HashMap { + let mut keys = HashMap::new(); + keys.insert("name".to_string(), self.name.clone()); + keys.insert("blockchain".to_string(), self.blockchain_name.clone()); + keys + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Wallet {} + +// Implement SledModel trait +impl SledModel for Wallet { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "wallet" + } +} \ No newline at end of file diff --git a/herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md b/herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md new file mode 100644 index 0000000..6e31a4b --- /dev/null +++ b/herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md @@ -0,0 +1,496 @@ +# Governance Module Enhancement Plan (Revised) + +## 1. Current State Analysis + +The governance module currently consists of: +- **Company**: Company model with basic company information +- **Shareholder**: Shareholder model for managing company ownership +- **Meeting**: Meeting and Attendee models for board meetings +- **User**: User model for system users +- **Vote**: Vote, VoteOption, and Ballot models for voting + +All models implement the `Storable` and `SledModel` traits for database integration, but the module has several limitations: +- Not imported in src/models/mod.rs, making it inaccessible to the rest of the project +- No mod.rs file to organize and re-export the types +- No README.md file to document the purpose and usage +- Inconsistent imports across files (e.g., crate::db vs crate::core) +- Limited utility methods and relationships between models +- No integration with other modules like biz, mcc, or circle + +## 2. Planned Enhancements + +### 2.1 Module Organization and Integration + +- Create a mod.rs file to organize and re-export the types +- Add the governance module to src/models/mod.rs +- Create a README.md file to document the purpose and usage +- Standardize imports across all files + +### 2.2 New Models + +#### 2.2.1 Resolution Model + +Create a new `resolution.rs` file with a Resolution model for managing board resolutions: +- Resolution information (title, description, text) +- Resolution status (Draft, Proposed, Approved, Rejected) +- Voting results and approvals +- Integration with Meeting and Vote models + +### 2.3 Enhanced Relationships and Integration + +#### 2.3.1 Integration with Biz Module + +- Link Company with biz::Customer and biz::Contract +- Link Shareholder with biz::Customer +- Link Meeting with biz::Invoice for expense tracking + +#### 2.3.2 Integration with MCC Module + +- Link Meeting with mcc::Calendar and mcc::Event +- Link User with mcc::Contact +- Link Vote with mcc::Message for notifications + +#### 2.3.3 Integration with Circle Module + +- Link Company with circle::Circle for group-based access control +- Link User with circle::Member for role-based permissions + +### 2.4 Utility Methods and Functionality + +- Add filtering and searching methods to all models +- Add relationship management methods between models +- Add validation and business logic methods + +## 3. Implementation Plan + +```mermaid +flowchart TD + A[Review Current Models] --> B[Create mod.rs and Update models/mod.rs] + B --> C[Standardize Imports and Fix Inconsistencies] + C --> D[Create Resolution Model] + D --> E[Implement Integration with Other Modules] + E --> F[Add Utility Methods] + F --> G[Create README.md and Documentation] + G --> H[Write Tests] +``` + +### 3.1 Detailed Changes + +#### 3.1.1 Module Organization + +Create a new `mod.rs` file in the governance directory: + +```rust +pub mod company; +pub mod shareholder; +pub mod meeting; +pub mod user; +pub mod vote; +pub mod resolution; + +// Re-export all model types for convenience +pub use company::{Company, CompanyStatus, BusinessType}; +pub use shareholder::{Shareholder, ShareholderType}; +pub use meeting::{Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus}; +pub use user::User; +pub use vote::{Vote, VoteOption, Ballot, VoteStatus}; +pub use resolution::{Resolution, ResolutionStatus, Approval}; + +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +``` + +Update `src/models/mod.rs` to include the governance module: + +```rust +pub mod biz; +pub mod mcc; +pub mod circle; +pub mod governance; +``` + +#### 3.1.2 Resolution Model (`resolution.rs`) + +```rust +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::gov::{Meeting, Vote}; + +/// ResolutionStatus represents the status of a resolution +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ResolutionStatus { + Draft, + Proposed, + Approved, + Rejected, + Withdrawn, +} + +/// Resolution represents a board resolution +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Resolution { + pub id: u32, + pub company_id: u32, + pub meeting_id: Option, + pub vote_id: Option, + pub title: String, + pub description: String, + pub text: String, + pub status: ResolutionStatus, + pub proposed_by: u32, // User ID + pub proposed_at: DateTime, + pub approved_at: Option>, + pub rejected_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, + pub approvals: Vec, +} + +/// Approval represents an approval of a resolution by a board member +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Approval { + pub id: u32, + pub resolution_id: u32, + pub user_id: u32, + pub name: String, + pub approved: bool, + pub comments: String, + pub created_at: DateTime, +} + +impl Resolution { + /// Create a new resolution with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + text: String, + proposed_by: u32, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + meeting_id: None, + vote_id: None, + title, + description, + text, + status: ResolutionStatus::Draft, + proposed_by, + proposed_at: now, + approved_at: None, + rejected_at: None, + created_at: now, + updated_at: now, + approvals: Vec::new(), + } + } + + /// Propose the resolution + pub fn propose(&mut self) { + self.status = ResolutionStatus::Proposed; + self.proposed_at = Utc::now(); + self.updated_at = Utc::now(); + } + + /// Approve the resolution + pub fn approve(&mut self) { + self.status = ResolutionStatus::Approved; + self.approved_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Reject the resolution + pub fn reject(&mut self) { + self.status = ResolutionStatus::Rejected; + self.rejected_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Add an approval to the resolution + pub fn add_approval(&mut self, user_id: u32, name: String, approved: bool, comments: String) -> &Approval { + let id = if self.approvals.is_empty() { + 1 + } else { + self.approvals.iter().map(|a| a.id).max().unwrap_or(0) + 1 + }; + + let approval = Approval { + id, + resolution_id: self.id, + user_id, + name, + approved, + comments, + created_at: Utc::now(), + }; + + self.approvals.push(approval); + self.updated_at = Utc::now(); + self.approvals.last().unwrap() + } + + /// Link this resolution to a meeting + pub fn link_to_meeting(&mut self, meeting_id: u32) { + self.meeting_id = Some(meeting_id); + self.updated_at = Utc::now(); + } + + /// Link this resolution to a vote + pub fn link_to_vote(&mut self, vote_id: u32) { + self.vote_id = Some(vote_id); + self.updated_at = Utc::now(); + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Resolution {} +impl Storable for Approval {} + +// Implement SledModel trait +impl SledModel for Resolution { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "resolution" + } +} +``` + +#### 3.1.3 Enhanced Company Model (`company.rs`) + +Add integration with other modules: + +```rust +impl Company { + // ... existing methods ... + + /// Link this company to a Circle for access control + pub fn link_to_circle(&mut self, circle_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Link this company to a Customer in the biz module + pub fn link_to_customer(&mut self, customer_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions for this company + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let company_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.company_id == self.id) + .collect(); + + Ok(company_resolutions) + } +} +``` + +#### 3.1.4 Enhanced Meeting Model (`meeting.rs`) + +Add integration with other modules: + +```rust +impl Meeting { + // ... existing methods ... + + /// Link this meeting to a Calendar Event in the mcc module + pub fn link_to_event(&mut self, event_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions discussed in this meeting + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let meeting_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.meeting_id == Some(self.id)) + .collect(); + + Ok(meeting_resolutions) + } +} +``` + +#### 3.1.5 Enhanced Vote Model (`vote.rs`) + +Add integration with Resolution model: + +```rust +impl Vote { + // ... existing methods ... + + /// Get the resolution associated with this vote + pub fn get_resolution(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let vote_resolution = all_resolutions + .into_iter() + .find(|resolution| resolution.vote_id == Some(self.id)); + + Ok(vote_resolution) + } +} +``` + +#### 3.1.6 Create README.md + +Create a README.md file to document the purpose and usage of the governance module. + +## 4. Data Model Diagram + +```mermaid +classDiagram + class Company { + +u32 id + +String name + +String registration_number + +DateTime incorporation_date + +String fiscal_year_end + +String email + +String phone + +String website + +String address + +BusinessType business_type + +String industry + +String description + +CompanyStatus status + +DateTime created_at + +DateTime updated_at + +add_shareholder() + +link_to_circle() + +link_to_customer() + +get_resolutions() + } + + class Shareholder { + +u32 id + +u32 company_id + +u32 user_id + +String name + +f64 shares + +f64 percentage + +ShareholderType type_ + +DateTime since + +DateTime created_at + +DateTime updated_at + +update_shares() + } + + class Meeting { + +u32 id + +u32 company_id + +String title + +DateTime date + +String location + +String description + +MeetingStatus status + +String minutes + +DateTime created_at + +DateTime updated_at + +Vec~Attendee~ attendees + +add_attendee() + +update_status() + +update_minutes() + +find_attendee_by_user_id() + +confirmed_attendees() + +link_to_event() + +get_resolutions() + } + + class User { + +u32 id + +String name + +String email + +String password + +String company + +String role + +DateTime created_at + +DateTime updated_at + } + + class Vote { + +u32 id + +u32 company_id + +String title + +String description + +DateTime start_date + +DateTime end_date + +VoteStatus status + +DateTime created_at + +DateTime updated_at + +Vec~VoteOption~ options + +Vec~Ballot~ ballots + +Vec~u32~ private_group + +add_option() + +add_ballot() + +get_resolution() + } + + class Resolution { + +u32 id + +u32 company_id + +Option~u32~ meeting_id + +Option~u32~ vote_id + +String title + +String description + +String text + +ResolutionStatus status + +u32 proposed_by + +DateTime proposed_at + +Option~DateTime~ approved_at + +Option~DateTime~ rejected_at + +DateTime created_at + +DateTime updated_at + +Vec~Approval~ approvals + +propose() + +approve() + +reject() + +add_approval() + +link_to_meeting() + +link_to_vote() + } + + Company "1" -- "many" Shareholder: has + Company "1" -- "many" Meeting: holds + Company "1" -- "many" Vote: conducts + Company "1" -- "many" Resolution: issues + Meeting "1" -- "many" Attendee: has + Meeting "1" -- "many" Resolution: discusses + Vote "1" -- "many" VoteOption: has + Vote "1" -- "many" Ballot: collects + Vote "1" -- "1" Resolution: decides + Resolution "1" -- "many" Approval: receives +``` + +## 5. Testing Strategy + +1. Unit tests for each model to verify: + - Basic functionality + - Serialization/deserialization + - Utility methods + - Integration with other models +2. Integration tests to verify: + - Database operations with the models + - Relationships between models + - Integration with other modules + +## 6. Future Considerations + +1. **Committee Model**: Add a Committee model in the future if needed +2. **Compliance Model**: Add compliance-related models in the future if needed +3. **API Integration**: Develop REST API endpoints for the governance module +4. **UI Components**: Create UI components for managing governance entities +5. **Reporting**: Implement reporting functionality for governance metrics \ No newline at end of file diff --git a/herodb/src/models/governance/README.md b/herodb/src/models/gov/README.md similarity index 100% rename from herodb/src/models/governance/README.md rename to herodb/src/models/gov/README.md diff --git a/herodb/src/models/governance/committee.rs b/herodb/src/models/gov/committee.rs similarity index 99% rename from herodb/src/models/governance/committee.rs rename to herodb/src/models/gov/committee.rs index 04ff00f..c7977f9 100644 --- a/herodb/src/models/governance/committee.rs +++ b/herodb/src/models/gov/committee.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::db::{SledModel, Storable, SledDB, SledDBError}; -use crate::models::governance::User; +use crate::models::gov::User; /// CommitteeRole represents the role of a member in a committee #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/herodb/src/models/governance/company.rs b/herodb/src/models/gov/company.rs similarity index 96% rename from herodb/src/models/governance/company.rs rename to herodb/src/models/gov/company.rs index cda4166..3c35fad 100644 --- a/herodb/src/models/governance/company.rs +++ b/herodb/src/models/gov/company.rs @@ -167,8 +167,8 @@ impl Company { } /// Get all resolutions for this company - pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { - let all_resolutions = db.list()?; + pub fn get_resolutions(&self, db: &crate::db::DB) -> Result, SledDBError> { + let all_resolutions = db.list::()?; let company_resolutions = all_resolutions .into_iter() .filter(|resolution| resolution.company_id == self.id) diff --git a/herodb/src/models/gov/compliance.rs b/herodb/src/models/gov/compliance.rs new file mode 100644 index 0000000..9f8063e --- /dev/null +++ b/herodb/src/models/gov/compliance.rs @@ -0,0 +1,212 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::gov::Company; + +/// ComplianceRequirement represents a regulatory requirement +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceRequirement { + pub id: u32, + pub company_id: u32, + pub title: String, + pub description: String, + pub regulation: String, + pub authority: String, + pub deadline: DateTime, + pub status: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// ComplianceDocument represents a compliance document +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceDocument { + pub id: u32, + pub requirement_id: u32, + pub title: String, + pub description: String, + pub file_path: String, + pub file_type: String, + pub uploaded_by: u32, // User ID + pub uploaded_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// ComplianceAudit represents a compliance audit +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceAudit { + pub id: u32, + pub company_id: u32, + pub title: String, + pub description: String, + pub auditor: String, + pub start_date: DateTime, + pub end_date: DateTime, + pub status: String, + pub findings: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl ComplianceRequirement { + /// Create a new compliance requirement with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + regulation: String, + authority: String, + deadline: DateTime, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + title, + description, + regulation, + authority, + deadline, + status: "Pending".to_string(), + created_at: now, + updated_at: now, + } + } + + /// Update the status of the requirement + pub fn update_status(&mut self, status: String) { + self.status = status; + self.updated_at = Utc::now(); + } + + /// Get the company associated with this requirement + pub fn get_company(&self, db: &SledDB) -> Result { + db.get(&self.company_id.to_string()) + } + + /// Get all documents associated with this requirement + pub fn get_documents(&self, db: &SledDB) -> Result, SledDBError> { + let all_documents = db.list()?; + let requirement_documents = all_documents + .into_iter() + .filter(|doc| doc.requirement_id == self.id) + .collect(); + + Ok(requirement_documents) + } +} + +impl ComplianceDocument { + /// Create a new compliance document with default values + pub fn new( + id: u32, + requirement_id: u32, + title: String, + description: String, + file_path: String, + file_type: String, + uploaded_by: u32, + ) -> Self { + let now = Utc::now(); + Self { + id, + requirement_id, + title, + description, + file_path, + file_type, + uploaded_by, + uploaded_at: now, + created_at: now, + updated_at: now, + } + } + + /// Get the requirement associated with this document + pub fn get_requirement(&self, db: &SledDB) -> Result { + db.get(&self.requirement_id.to_string()) + } +} + +impl ComplianceAudit { + /// Create a new compliance audit with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + auditor: String, + start_date: DateTime, + end_date: DateTime, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + title, + description, + auditor, + start_date, + end_date, + status: "Planned".to_string(), + findings: String::new(), + created_at: now, + updated_at: now, + } + } + + /// Update the status of the audit + pub fn update_status(&mut self, status: String) { + self.status = status; + self.updated_at = Utc::now(); + } + + /// Update the findings of the audit + pub fn update_findings(&mut self, findings: String) { + self.findings = findings; + self.updated_at = Utc::now(); + } + + /// Get the company associated with this audit + pub fn get_company(&self, db: &SledDB) -> Result { + db.get(&self.company_id.to_string()) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for ComplianceRequirement {} +impl Storable for ComplianceDocument {} +impl Storable for ComplianceAudit {} + +// Implement SledModel trait +impl SledModel for ComplianceRequirement { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_requirement" + } +} + +impl SledModel for ComplianceDocument { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_document" + } +} + +impl SledModel for ComplianceAudit { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_audit" + } +} \ No newline at end of file diff --git a/herodb/src/models/governance/meeting.rs b/herodb/src/models/gov/meeting.rs similarity index 96% rename from herodb/src/models/governance/meeting.rs rename to herodb/src/models/gov/meeting.rs index 6b15ef7..c355840 100644 --- a/herodb/src/models/governance/meeting.rs +++ b/herodb/src/models/gov/meeting.rs @@ -164,8 +164,8 @@ impl Meeting { } /// Get all resolutions discussed in this meeting - pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { - let all_resolutions = db.list()?; + pub fn get_resolutions(&self, db: &crate::db::DB) -> Result, SledDBError> { + let all_resolutions = db.list::()?; let meeting_resolutions = all_resolutions .into_iter() .filter(|resolution| resolution.meeting_id == Some(self.id)) diff --git a/herodb/src/models/governance/mod.rs b/herodb/src/models/gov/mod.rs similarity index 100% rename from herodb/src/models/governance/mod.rs rename to herodb/src/models/gov/mod.rs diff --git a/herodb/src/models/governance/resolution.rs b/herodb/src/models/gov/resolution.rs similarity index 93% rename from herodb/src/models/governance/resolution.rs rename to herodb/src/models/gov/resolution.rs index 59fe9a8..1416e35 100644 --- a/herodb/src/models/governance/resolution.rs +++ b/herodb/src/models/gov/resolution.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::db::{SledModel, Storable, SledDB, SledDBError}; -use crate::models::governance::{Meeting, Vote}; +use crate::models::gov::{Meeting, Vote}; /// ResolutionStatus represents the status of a resolution #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -158,10 +158,10 @@ impl Resolution { } /// Get the meeting associated with this resolution - pub fn get_meeting(&self, db: &SledDB) -> Result, SledDBError> { + pub fn get_meeting(&self, db: &crate::db::DB) -> Result, SledDBError> { match self.meeting_id { Some(meeting_id) => { - let meeting = db.get(&meeting_id.to_string())?; + let meeting = db.get::(&meeting_id.to_string())?; Ok(Some(meeting)) } None => Ok(None), @@ -169,10 +169,10 @@ impl Resolution { } /// Get the vote associated with this resolution - pub fn get_vote(&self, db: &SledDB) -> Result, SledDBError> { + pub fn get_vote(&self, db: &crate::db::DB) -> Result, SledDBError> { match self.vote_id { Some(vote_id) => { - let vote = db.get(&vote_id.to_string())?; + let vote = db.get::(&vote_id.to_string())?; Ok(Some(vote)) } None => Ok(None), diff --git a/herodb/src/models/governance/shareholder.rs b/herodb/src/models/gov/shareholder.rs similarity index 100% rename from herodb/src/models/governance/shareholder.rs rename to herodb/src/models/gov/shareholder.rs diff --git a/herodb/src/models/governance/user.rs b/herodb/src/models/gov/user.rs similarity index 100% rename from herodb/src/models/governance/user.rs rename to herodb/src/models/gov/user.rs diff --git a/herodb/src/models/governance/vote.rs b/herodb/src/models/gov/vote.rs similarity index 95% rename from herodb/src/models/governance/vote.rs rename to herodb/src/models/gov/vote.rs index 5922e64..9e4cc4a 100644 --- a/herodb/src/models/governance/vote.rs +++ b/herodb/src/models/gov/vote.rs @@ -128,8 +128,8 @@ impl Vote { } /// Get the resolution associated with this vote - pub fn get_resolution(&self, db: &SledDB) -> Result, SledDBError> { - let all_resolutions = db.list()?; + pub fn get_resolution(&self, db: &crate::db::DB) -> Result, SledDBError> { + let all_resolutions = db.list::()?; let vote_resolution = all_resolutions .into_iter() .find(|resolution| resolution.vote_id == Some(self.id)); diff --git a/herodb/src/models/instructions.md b/herodb/src/models/instructions.md new file mode 100644 index 0000000..3a2cfc7 --- /dev/null +++ b/herodb/src/models/instructions.md @@ -0,0 +1,18 @@ +in @src/models/circle/circle.rs + +- member us now new rootobject, check implementation +- a member is linked to one or more contacts id's (from src/models/mcc/contacts.rs) +- a member has one or more wallets + + +in@src/models/biz add a ticket module + +user can have more than 1 ticket which is to ask support from the org + +a ticket has following fields + +- subject +- description +- creation/update date +- assignees (based on memberid see above) +- \ No newline at end of file diff --git a/herodb/src/models/mod.rs b/herodb/src/models/mod.rs index bc6d16a..1c3df0f 100644 --- a/herodb/src/models/mod.rs +++ b/herodb/src/models/mod.rs @@ -1,4 +1,4 @@ pub mod biz; pub mod mcc; pub mod circle; -pub mod governance; \ No newline at end of file +pub mod gov; \ No newline at end of file diff --git a/herodb/src/models/py/README.md b/herodb/src/models/py/README.md new file mode 100644 index 0000000..07f9a9c --- /dev/null +++ b/herodb/src/models/py/README.md @@ -0,0 +1,131 @@ +# Business Models Python Port + +This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration. + +## Overview + +This project includes: + +1. Python port of Rust business models using SQLModel +2. FastAPI server with OpenAPI/Swagger documentation +3. CRUD operations for all models +4. Convenience endpoints for common operations + +The models ported from Rust to Python include: + +- **Currency**: Represents a monetary value with amount and currency code +- **Customer**: Represents a customer who can purchase products or services +- **Product**: Represents a product or service offered +- **ProductComponent**: Represents a component of a product +- **SaleItem**: Represents an item in a sale +- **Sale**: Represents a sale of products or services + +## Structure + +- `models.py`: Contains the SQLModel definitions for all business models +- `example.py`: Demonstrates how to use the models with a sample application +- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example +- `api.py`: FastAPI server providing CRUD operations for all models +- `server.sh`: Bash script to start the FastAPI server + +## Requirements + +- Python 3.7+ +- [uv](https://github.com/astral-sh/uv) for dependency management + +## Installation + +The project uses `uv` for dependency management. To install dependencies and run the example: + +```bash +./install_and_run.sh +``` + +## API Server + +The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints. + +### Starting the Server + +To start the API server: + +```bash +./server.sh +``` + +This script will: +1. Create a virtual environment if it doesn't exist +2. Install the required dependencies using `uv` +3. Start the FastAPI server with hot reloading enabled + +### API Documentation + +Once the server is running, you can access the OpenAPI documentation at: + +- Swagger UI: http://localhost:8000/docs +- ReDoc: http://localhost:8000/redoc + +### Available Endpoints + +The API provides the following endpoints: + +#### Currencies +- `GET /currencies/`: List all currencies +- `POST /currencies/`: Create a new currency +- `GET /currencies/{currency_id}`: Get a specific currency +- `PUT /currencies/{currency_id}`: Update a currency +- `DELETE /currencies/{currency_id}`: Delete a currency + +#### Customers +- `GET /customers/`: List all customers +- `POST /customers/`: Create a new customer +- `GET /customers/{customer_id}`: Get a specific customer +- `PUT /customers/{customer_id}`: Update a customer +- `DELETE /customers/{customer_id}`: Delete a customer +- `GET /customers/{customer_id}/sales/`: Get all sales for a customer + +#### Products +- `GET /products/`: List all products +- `POST /products/`: Create a new product +- `GET /products/{product_id}`: Get a specific product +- `PUT /products/{product_id}`: Update a product +- `DELETE /products/{product_id}`: Delete a product +- `GET /products/available/`: Get all available products +- `POST /products/{product_id}/components/`: Add a component to a product +- `GET /products/{product_id}/components/`: Get all components for a product + +#### Sales +- `GET /sales/`: List all sales +- `POST /sales/`: Create a new sale +- `GET /sales/{sale_id}`: Get a specific sale +- `PUT /sales/{sale_id}`: Update a sale +- `DELETE /sales/{sale_id}`: Delete a sale +- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale +- `POST /sales/{sale_id}/items/`: Add an item to a sale +- `GET /sales/{sale_id}/items/`: Get all items for a sale + +## Dependencies + +- SQLModel: For database models and ORM functionality +- Pydantic: For data validation (used by SQLModel) +- FastAPI: For creating the API server +- Uvicorn: ASGI server for running FastAPI applications + +## Example Usage + +The `example.py` script demonstrates: + +1. Creating an SQLite database +2. Defining and creating tables for the models +3. Creating sample data (customers, products, sales) +4. Performing operations on the data +5. Querying and displaying the data + +To run the example manually (after activating the virtual environment): + +```bash +# From the py directory +python example.py + +# Or from the parent directory +cd py && python example.py \ No newline at end of file diff --git a/herodb/src/models/py/__init__.py b/herodb/src/models/py/__init__.py new file mode 100644 index 0000000..b4c11ea --- /dev/null +++ b/herodb/src/models/py/__init__.py @@ -0,0 +1,3 @@ +""" +Python port of the business models from Rust. +""" \ No newline at end of file diff --git a/herodb/src/models/py/__pycache__/api.cpython-312.pyc b/herodb/src/models/py/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..4dc5653 Binary files /dev/null and b/herodb/src/models/py/__pycache__/api.cpython-312.pyc differ diff --git a/herodb/src/models/py/__pycache__/models.cpython-312.pyc b/herodb/src/models/py/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..df1521d Binary files /dev/null and b/herodb/src/models/py/__pycache__/models.cpython-312.pyc differ diff --git a/herodb/src/models/py/api.py b/herodb/src/models/py/api.py new file mode 100755 index 0000000..1b09bd2 --- /dev/null +++ b/herodb/src/models/py/api.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +""" +FastAPI server providing CRUD operations for business models. +""" +import os +from datetime import datetime +from typing import List, Optional + +from fastapi import Depends, FastAPI, HTTPException, Query +from fastapi.middleware.cors import CORSMiddleware +from sqlmodel import Session, SQLModel, create_engine, select + +from models import ( + Currency, + Customer, + Product, + ProductComponent, + ProductStatus, + ProductType, + Sale, + SaleItem, + SaleStatus, +) + +# Create database +DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db") +engine = create_engine(DATABASE_URL, echo=False) + +# Create tables +SQLModel.metadata.create_all(engine) + +# Create FastAPI app +app = FastAPI( + title="Business API", + description="API for business models", + version="1.0.0", + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Dependency to get database session +def get_session(): + with Session(engine) as session: + yield session + + +# Root endpoint +@app.get("/") +async def root(): + return {"message": "Welcome to the Business API"} + + +# Currency endpoints +@app.post("/currencies/", response_model=Currency, tags=["Currencies"]) +def create_currency(currency: Currency, session: Session = Depends(get_session)): + """Create a new currency""" + session.add(currency) + session.commit() + session.refresh(currency) + return currency + + +@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"]) +def read_currencies( + skip: int = 0, limit: int = 100, session: Session = Depends(get_session) +): + """Get all currencies""" + currencies = session.exec(select(Currency).offset(skip).limit(limit)).all() + return currencies + + +@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"]) +def read_currency(currency_id: int, session: Session = Depends(get_session)): + """Get a currency by ID""" + currency = session.get(Currency, currency_id) + if not currency: + raise HTTPException(status_code=404, detail="Currency not found") + return currency + + +@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"]) +def update_currency( + currency_id: int, currency_data: Currency, session: Session = Depends(get_session) +): + """Update a currency""" + currency = session.get(Currency, currency_id) + if not currency: + raise HTTPException(status_code=404, detail="Currency not found") + + # Update currency attributes + currency_data_dict = currency_data.dict(exclude_unset=True) + for key, value in currency_data_dict.items(): + setattr(currency, key, value) + + session.add(currency) + session.commit() + session.refresh(currency) + return currency + + +@app.delete("/currencies/{currency_id}", tags=["Currencies"]) +def delete_currency(currency_id: int, session: Session = Depends(get_session)): + """Delete a currency""" + currency = session.get(Currency, currency_id) + if not currency: + raise HTTPException(status_code=404, detail="Currency not found") + + session.delete(currency) + session.commit() + return {"message": "Currency deleted successfully"} + + +# Customer endpoints +@app.post("/customers/", response_model=Customer, tags=["Customers"]) +def create_customer(customer: Customer, session: Session = Depends(get_session)): + """Create a new customer""" + session.add(customer) + session.commit() + session.refresh(customer) + return customer + + +@app.get("/customers/", response_model=List[Customer], tags=["Customers"]) +def read_customers( + skip: int = 0, limit: int = 100, session: Session = Depends(get_session) +): + """Get all customers""" + customers = session.exec(select(Customer).offset(skip).limit(limit)).all() + return customers + + +@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"]) +def read_customer(customer_id: int, session: Session = Depends(get_session)): + """Get a customer by ID""" + customer = session.get(Customer, customer_id) + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + return customer + + +@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"]) +def update_customer( + customer_id: int, customer_data: Customer, session: Session = Depends(get_session) +): + """Update a customer""" + customer = session.get(Customer, customer_id) + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + + # Update customer attributes + customer_data_dict = customer_data.dict(exclude_unset=True) + for key, value in customer_data_dict.items(): + setattr(customer, key, value) + + session.add(customer) + session.commit() + session.refresh(customer) + return customer + + +@app.delete("/customers/{customer_id}", tags=["Customers"]) +def delete_customer(customer_id: int, session: Session = Depends(get_session)): + """Delete a customer""" + customer = session.get(Customer, customer_id) + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + + session.delete(customer) + session.commit() + return {"message": "Customer deleted successfully"} + + +# Product endpoints +@app.post("/products/", response_model=Product, tags=["Products"]) +def create_product(product: Product, session: Session = Depends(get_session)): + """Create a new product""" + session.add(product) + session.commit() + session.refresh(product) + return product + + +@app.get("/products/", response_model=List[Product], tags=["Products"]) +def read_products( + skip: int = 0, + limit: int = 100, + category: Optional[str] = None, + status: Optional[ProductStatus] = None, + istemplate: Optional[bool] = None, + session: Session = Depends(get_session) +): + """Get all products with optional filtering""" + query = select(Product) + + if category: + query = query.where(Product.category == category) + + if status: + query = query.where(Product.status == status) + + if istemplate is not None: + query = query.where(Product.istemplate == istemplate) + + products = session.exec(query.offset(skip).limit(limit)).all() + return products + + +@app.get("/products/{product_id}", response_model=Product, tags=["Products"]) +def read_product(product_id: int, session: Session = Depends(get_session)): + """Get a product by ID""" + product = session.get(Product, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + +@app.put("/products/{product_id}", response_model=Product, tags=["Products"]) +def update_product( + product_id: int, product_data: Product, session: Session = Depends(get_session) +): + """Update a product""" + product = session.get(Product, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + + # Update product attributes + product_data_dict = product_data.dict(exclude_unset=True) + for key, value in product_data_dict.items(): + setattr(product, key, value) + + session.add(product) + session.commit() + session.refresh(product) + return product + + +@app.delete("/products/{product_id}", tags=["Products"]) +def delete_product(product_id: int, session: Session = Depends(get_session)): + """Delete a product""" + product = session.get(Product, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + + session.delete(product) + session.commit() + return {"message": "Product deleted successfully"} + + +# Product Component endpoints +@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"]) +def create_product_component( + product_id: int, component: ProductComponent, session: Session = Depends(get_session) +): + """Add a component to a product""" + product = session.get(Product, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + + component.product_id = product_id + session.add(component) + session.commit() + session.refresh(component) + return component + + +@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"]) +def read_product_components( + product_id: int, session: Session = Depends(get_session) +): + """Get all components for a product""" + product = session.get(Product, product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + + return product.components + + +# Sale endpoints +@app.post("/sales/", response_model=Sale, tags=["Sales"]) +def create_sale(sale: Sale, session: Session = Depends(get_session)): + """Create a new sale""" + # Ensure customer exists if customer_id is provided + if sale.customer_id: + customer = session.get(Customer, sale.customer_id) + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + + session.add(sale) + session.commit() + session.refresh(sale) + return sale + + +@app.get("/sales/", response_model=List[Sale], tags=["Sales"]) +def read_sales( + skip: int = 0, + limit: int = 100, + status: Optional[SaleStatus] = None, + customer_id: Optional[int] = None, + session: Session = Depends(get_session) +): + """Get all sales with optional filtering""" + query = select(Sale) + + if status: + query = query.where(Sale.status == status) + + if customer_id: + query = query.where(Sale.customer_id == customer_id) + + sales = session.exec(query.offset(skip).limit(limit)).all() + return sales + + +@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"]) +def read_sale(sale_id: int, session: Session = Depends(get_session)): + """Get a sale by ID""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + return sale + + +@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"]) +def update_sale( + sale_id: int, sale_data: Sale, session: Session = Depends(get_session) +): + """Update a sale""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + + # Update sale attributes + sale_data_dict = sale_data.dict(exclude_unset=True) + for key, value in sale_data_dict.items(): + setattr(sale, key, value) + + session.add(sale) + session.commit() + session.refresh(sale) + return sale + + +@app.delete("/sales/{sale_id}", tags=["Sales"]) +def delete_sale(sale_id: int, session: Session = Depends(get_session)): + """Delete a sale""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + + session.delete(sale) + session.commit() + return {"message": "Sale deleted successfully"} + + +# Sale Item endpoints +@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"]) +def create_sale_item( + sale_id: int, item: SaleItem, session: Session = Depends(get_session) +): + """Add an item to a sale""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + + item.sale_id = sale_id + session.add(item) + session.commit() + session.refresh(item) + + # Update the sale's total amount + sale.add_item(item) + session.add(sale) + session.commit() + + return item + + +@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"]) +def read_sale_items( + sale_id: int, session: Session = Depends(get_session) +): + """Get all items for a sale""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + + return sale.items + + +# Convenience endpoints +@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"]) +def update_sale_status( + sale_id: int, status: SaleStatus, session: Session = Depends(get_session) +): + """Update the status of a sale""" + sale = session.get(Sale, sale_id) + if not sale: + raise HTTPException(status_code=404, detail="Sale not found") + + sale.update_status(status) + session.add(sale) + session.commit() + session.refresh(sale) + return sale + + +@app.get("/products/available/", response_model=List[Product], tags=["Convenience"]) +def get_available_products( + istemplate: Optional[bool] = False, + session: Session = Depends(get_session) +): + """Get all available products""" + query = select(Product).where( + Product.status == ProductStatus.AVAILABLE, + Product.purchase_till > datetime.utcnow(), + Product.istemplate == istemplate + ) + products = session.exec(query).all() + return products + + +@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"]) +def get_customer_sales( + customer_id: int, + status: Optional[SaleStatus] = None, + session: Session = Depends(get_session) +): + """Get all sales for a customer""" + customer = session.get(Customer, customer_id) + if not customer: + raise HTTPException(status_code=404, detail="Customer not found") + + query = select(Sale).where(Sale.customer_id == customer_id) + + if status: + query = query.where(Sale.status == status) + + sales = session.exec(query).all() + return sales + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/herodb/src/models/py/business.db b/herodb/src/models/py/business.db new file mode 100644 index 0000000..a304f09 Binary files /dev/null and b/herodb/src/models/py/business.db differ diff --git a/herodb/src/models/py/example.py b/herodb/src/models/py/example.py new file mode 100755 index 0000000..37d3f56 --- /dev/null +++ b/herodb/src/models/py/example.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Example script demonstrating the use of the business models. +""" +import datetime +from typing import List + +from sqlmodel import Session, SQLModel, create_engine, select + +from models import ( + Currency, + Customer, + Product, + ProductComponent, + ProductStatus, + ProductType, + Sale, + SaleItem, + SaleStatus, +) + + +def create_tables(engine): + """Create all tables in the database""" + SQLModel.metadata.create_all(engine) + + +def create_sample_data(session: Session) -> None: + """Create sample data for demonstration""" + # Create currencies + usd = Currency(currency_code="USD", amount=0.0) + eur = Currency(currency_code="EUR", amount=0.0) + session.add(usd) + session.add(eur) + session.commit() + + # Create a customer + customer = Customer.new( + name="Acme Corporation", + description="A fictional company", + pubkey="acme123456", + contact_sids=["circle1_contact123", "circle2_contact456"] + ) + session.add(customer) + session.commit() + + # Create product components + cpu_component = ProductComponent.new( + name="CPU", + description="Central Processing Unit", + quantity=1, + ) + ram_component = ProductComponent.new( + name="RAM", + description="Random Access Memory", + quantity=2, + ) + session.add(cpu_component) + session.add(ram_component) + session.commit() + + # Create products + laptop_price = Currency(currency_code="USD", amount=1200.0) + session.add(laptop_price) + session.commit() + + laptop = Product.new( + name="Laptop", + description="High-performance laptop", + price=laptop_price, + type_=ProductType.PRODUCT, + category="Electronics", + status=ProductStatus.AVAILABLE, + max_amount=100, + validity_days=365, + istemplate=False, + ) + laptop.add_component(cpu_component) + laptop.add_component(ram_component) + session.add(laptop) + session.commit() + + support_price = Currency(currency_code="USD", amount=50.0) + session.add(support_price) + session.commit() + + support = Product.new( + name="Technical Support", + description="24/7 technical support", + price=support_price, + type_=ProductType.SERVICE, + category="Support", + status=ProductStatus.AVAILABLE, + max_amount=1000, + validity_days=30, + istemplate=True, # This is a template product + ) + session.add(support) + session.commit() + + # Create a sale + sale = Sale.new( + customer=customer, + currency_code="USD", + ) + session.add(sale) + session.commit() + + # Create sale items + laptop_unit_price = Currency(currency_code="USD", amount=1200.0) + session.add(laptop_unit_price) + session.commit() + + laptop_item = SaleItem.new( + product=laptop, + quantity=1, + unit_price=laptop_unit_price, + active_till=datetime.datetime.utcnow() + datetime.timedelta(days=365), + ) + sale.add_item(laptop_item) + + support_unit_price = Currency(currency_code="USD", amount=50.0) + session.add(support_unit_price) + session.commit() + + support_item = SaleItem.new( + product=support, + quantity=2, + unit_price=support_unit_price, + active_till=datetime.datetime.utcnow() + datetime.timedelta(days=30), + ) + sale.add_item(support_item) + + # Complete the sale + sale.update_status(SaleStatus.COMPLETED) + session.commit() + + +def query_data(session: Session) -> None: + """Query and display data from the database""" + print("\n=== Customers ===") + customers = session.exec(select(Customer)).all() + for customer in customers: + print(f"Customer: {customer.name} ({customer.pubkey})") + print(f" Description: {customer.description}") + print(f" Contact SIDs: {', '.join(customer.contact_sids)}") + print(f" Created at: {customer.created_at}") + + print("\n=== Products ===") + products = session.exec(select(Product)).all() + for product in products: + print(f"Product: {product.name} ({product.type_.value})") + print(f" Description: {product.description}") + print(f" Price: {product.price.amount} {product.price.currency_code}") + print(f" Status: {product.status.value}") + print(f" Is Template: {product.istemplate}") + print(f" Components:") + for component in product.components: + print(f" - {component.name}: {component.quantity}") + + print("\n=== Sales ===") + sales = session.exec(select(Sale)).all() + for sale in sales: + print(f"Sale to: {sale.customer.name}") + print(f" Status: {sale.status.value}") + print(f" Total: {sale.total_amount.amount} {sale.total_amount.currency_code}") + print(f" Items:") + for item in sale.items: + print(f" - {item.name}: {item.quantity} x {item.unit_price.amount} = {item.subtotal.amount} {item.subtotal.currency_code}") + + +def main(): + """Main function""" + print("Creating in-memory SQLite database...") + engine = create_engine("sqlite:///business.db", echo=False) + + print("Creating tables...") + create_tables(engine) + + print("Creating sample data...") + with Session(engine) as session: + create_sample_data(session) + + print("Querying data...") + with Session(engine) as session: + query_data(session) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/herodb/src/models/py/install_and_run.sh b/herodb/src/models/py/install_and_run.sh new file mode 100755 index 0000000..103c3e8 --- /dev/null +++ b/herodb/src/models/py/install_and_run.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Script to install dependencies using uv and run the example script + +set -e # Exit on error + +# Change to the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" +echo "Changed to directory: $SCRIPT_DIR" + +# Define variables +VENV_DIR=".venv" +REQUIREMENTS="sqlmodel pydantic" + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo "Error: uv is not installed." + echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh" + exit 1 +fi + +# Create virtual environment if it doesn't exist +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + uv venv "$VENV_DIR" +fi + +# Activate virtual environment +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +# Install dependencies +echo "Installing dependencies using uv..." +uv pip install $REQUIREMENTS + +# Make example.py executable +chmod +x example.py + +# Remove existing database file if it exists +if [ -f "business.db" ]; then + echo "Removing existing database file..." + rm business.db +fi + +# Run the example script +echo "Running example script..." +python example.py + +echo "Done!" \ No newline at end of file diff --git a/herodb/src/models/py/models.py b/herodb/src/models/py/models.py new file mode 100644 index 0000000..d396a2f --- /dev/null +++ b/herodb/src/models/py/models.py @@ -0,0 +1,297 @@ +""" +Python port of the business models from Rust using SQLModel. +""" +from datetime import datetime, timedelta +from enum import Enum +import json +from typing import List, Optional + +from sqlmodel import Field, Relationship, SQLModel + + +class SaleStatus(str, Enum): + """SaleStatus represents the status of a sale""" + PENDING = "pending" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class ProductType(str, Enum): + """ProductType represents the type of a product""" + PRODUCT = "product" + SERVICE = "service" + + +class ProductStatus(str, Enum): + """ProductStatus represents the status of a product""" + AVAILABLE = "available" + UNAVAILABLE = "unavailable" + + +class Currency(SQLModel, table=True): + """Currency represents a monetary value with amount and currency code""" + id: Optional[int] = Field(default=None, primary_key=True) + amount: float + currency_code: str + + @classmethod + def new(cls, amount: float, currency_code: str) -> "Currency": + """Create a new currency with amount and code""" + return cls(amount=amount, currency_code=currency_code) + + +class Customer(SQLModel, table=True): + """Customer represents a customer who can purchase products or services""" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + description: str + pubkey: str + contact_sids_json: str = Field(default="[]") + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + sales: List["Sale"] = Relationship(back_populates="customer") + + @property + def contact_sids(self) -> List[str]: + """Get the contact SIDs as a list""" + return json.loads(self.contact_sids_json) + + @contact_sids.setter + def contact_sids(self, value: List[str]) -> None: + """Set the contact SIDs from a list""" + self.contact_sids_json = json.dumps(value) + + @classmethod + def new(cls, name: str, description: str, pubkey: str, contact_sids: List[str] = None) -> "Customer": + """Create a new customer with default timestamps""" + customer = cls( + name=name, + description=description, + pubkey=pubkey, + ) + if contact_sids: + customer.contact_sids = contact_sids + return customer + + def add_contact(self, contact_id: int) -> None: + """Add a contact ID to the customer""" + # In a real implementation, this would add a relationship to a Contact model + # For simplicity, we're not implementing the Contact model in this example + self.updated_at = datetime.utcnow() + + def add_contact_sid(self, circle_id: str, object_id: str) -> None: + """Add a smart ID (sid) to the customer's contact_sids list""" + sid = f"{circle_id}_{object_id}" + sids = self.contact_sids + if sid not in sids: + sids.append(sid) + self.contact_sids = sids + self.updated_at = datetime.utcnow() + + +class ProductComponent(SQLModel, table=True): + """ProductComponent represents a component of a product""" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + description: str + quantity: int + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + product_id: Optional[int] = Field(default=None, foreign_key="product.id") + product: Optional["Product"] = Relationship(back_populates="components") + + @classmethod + def new(cls, name: str, description: str, quantity: int) -> "ProductComponent": + """Create a new product component with default timestamps""" + return cls( + name=name, + description=description, + quantity=quantity, + ) + + +class Product(SQLModel, table=True): + """Product represents a product or service offered""" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + description: str + type_: ProductType = Field(sa_column_kwargs={"name": "type"}) + category: str + status: ProductStatus + max_amount: int + purchase_till: datetime + active_till: datetime + istemplate: bool = Field(default=False) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Price relationship + price_id: Optional[int] = Field(default=None, foreign_key="currency.id") + price: Optional[Currency] = Relationship() + + # Relationships + components: List[ProductComponent] = Relationship(back_populates="product") + sale_items: List["SaleItem"] = Relationship(back_populates="product") + + @classmethod + def new( + cls, + name: str, + description: str, + price: Currency, + type_: ProductType, + category: str, + status: ProductStatus, + max_amount: int, + validity_days: int, + istemplate: bool = False, + ) -> "Product": + """Create a new product with default timestamps""" + now = datetime.utcnow() + return cls( + name=name, + description=description, + price=price, + type_=type_, + category=category, + status=status, + max_amount=max_amount, + purchase_till=now + timedelta(days=365), + active_till=now + timedelta(days=validity_days), + istemplate=istemplate, + ) + + def add_component(self, component: ProductComponent) -> None: + """Add a component to this product""" + component.product = self + self.components.append(component) + self.updated_at = datetime.utcnow() + + def set_purchase_period(self, purchase_till: datetime) -> None: + """Update the purchase availability timeframe""" + self.purchase_till = purchase_till + self.updated_at = datetime.utcnow() + + def set_active_period(self, active_till: datetime) -> None: + """Update the active timeframe""" + self.active_till = active_till + self.updated_at = datetime.utcnow() + + def is_purchasable(self) -> bool: + """Check if the product is available for purchase""" + return self.status == ProductStatus.AVAILABLE and datetime.utcnow() <= self.purchase_till + + def is_active(self) -> bool: + """Check if the product is still active (for services)""" + return datetime.utcnow() <= self.active_till + + +class SaleItem(SQLModel, table=True): + """SaleItem represents an item in a sale""" + id: Optional[int] = Field(default=None, primary_key=True) + name: str + quantity: int + active_till: datetime + + # Relationships + sale_id: Optional[int] = Field(default=None, foreign_key="sale.id") + sale: Optional["Sale"] = Relationship(back_populates="items") + + product_id: Optional[int] = Field(default=None, foreign_key="product.id") + product: Optional[Product] = Relationship(back_populates="sale_items") + + unit_price_id: Optional[int] = Field(default=None, foreign_key="currency.id") + unit_price: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.unit_price_id]"}) + + subtotal_id: Optional[int] = Field(default=None, foreign_key="currency.id") + subtotal: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.subtotal_id]"}) + + @classmethod + def new( + cls, + product: Product, + quantity: int, + unit_price: Currency, + active_till: datetime, + ) -> "SaleItem": + """Create a new sale item""" + # Calculate subtotal + amount = unit_price.amount * quantity + subtotal = Currency( + amount=amount, + currency_code=unit_price.currency_code, + ) + + return cls( + name=product.name, + product=product, + quantity=quantity, + unit_price=unit_price, + subtotal=subtotal, + active_till=active_till, + ) + + +class Sale(SQLModel, table=True): + """Sale represents a sale of products or services""" + id: Optional[int] = Field(default=None, primary_key=True) + status: SaleStatus + sale_date: datetime = Field(default_factory=datetime.utcnow) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: datetime = Field(default_factory=datetime.utcnow) + + # Relationships + customer_id: Optional[int] = Field(default=None, foreign_key="customer.id") + customer: Optional[Customer] = Relationship(back_populates="sales") + + total_amount_id: Optional[int] = Field(default=None, foreign_key="currency.id") + total_amount: Optional[Currency] = Relationship() + + items: List[SaleItem] = Relationship(back_populates="sale") + + @classmethod + def new( + cls, + customer: Customer, + currency_code: str, + status: SaleStatus = SaleStatus.PENDING, + ) -> "Sale": + """Create a new sale with default timestamps""" + total_amount = Currency(amount=0.0, currency_code=currency_code) + + return cls( + customer=customer, + total_amount=total_amount, + status=status, + ) + + def add_item(self, item: SaleItem) -> None: + """Add an item to the sale and update the total amount""" + item.sale = self + + # Update the total amount + if not self.items: + # First item, initialize the total amount with the same currency + self.total_amount = Currency( + amount=item.subtotal.amount, + currency_code=item.subtotal.currency_code, + ) + else: + # Add to the existing total + # (Assumes all items have the same currency) + self.total_amount.amount += item.subtotal.amount + + # Add the item to the list + self.items.append(item) + + # Update the sale timestamp + self.updated_at = datetime.utcnow() + + def update_status(self, status: SaleStatus) -> None: + """Update the status of the sale""" + self.status = status + self.updated_at = datetime.utcnow() \ No newline at end of file diff --git a/herodb/src/models/py/server.sh b/herodb/src/models/py/server.sh new file mode 100755 index 0000000..d291078 --- /dev/null +++ b/herodb/src/models/py/server.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Script to start the FastAPI server + +set -e # Exit on error + +# Change to the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" +echo "Changed to directory: $SCRIPT_DIR" + +# Define variables +VENV_DIR=".venv" +REQUIREMENTS="sqlmodel pydantic fastapi uvicorn" + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo "Error: uv is not installed." + echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh" + exit 1 +fi + +# Create virtual environment if it doesn't exist +if [ ! -d "$VENV_DIR" ]; then + echo "Creating virtual environment..." + uv venv "$VENV_DIR" +fi + +# Activate virtual environment +echo "Activating virtual environment..." +source "$VENV_DIR/bin/activate" + +# Install dependencies +echo "Installing dependencies using uv..." +uv pip install $REQUIREMENTS + +# Make api.py executable +chmod +x api.py + +# Start the FastAPI server +echo "Starting FastAPI server..." +echo "API documentation available at: http://localhost:8000/docs" +uvicorn api:app --host 0.0.0.0 --port 8000 --reload \ No newline at end of file diff --git a/herodb/src/rhaiengine/mod.rs b/herodb/src/rhaiengine/mod.rs new file mode 100644 index 0000000..57dee57 --- /dev/null +++ b/herodb/src/rhaiengine/mod.rs @@ -0,0 +1,7 @@ +//! Rhai Engine module for scripting support +//! +//! This module provides integration with the Rhai scripting language. + +// Re-export the engine module +pub mod engine; +pub use engine::*; \ No newline at end of file