Merge branch 'builders_in_script'
* builders_in_script: .... ... ... ... ... ... ... ... ... Inject some builders in script # Conflicts: # herodb/src/cmd/dbexample/examples.rs # herodb/src/models/biz/product.rs # herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md # herodb/src/models/gov/compliance.rs
This commit is contained in:
		@@ -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<T>`: 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::<User>()
 | 
			
		||||
    .register_model::<Company>()
 | 
			
		||||
    .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::<User>()?;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 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<T>` 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
 | 
			
		||||
@@ -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<Utc>,
 | 
			
		||||
        updated_at: DateTime<Utc>,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<P: Into<PathBuf>>(path: P) -> SledDBResult<DB> {
 | 
			
		||||
    // Using the builder pattern to register all models
 | 
			
		||||
    DBBuilder::new(path)
 | 
			
		||||
        .register_model::<User>()
 | 
			
		||||
        .register_model::<Company>()
 | 
			
		||||
        .register_model::<Meeting>()
 | 
			
		||||
        .register_model::<Product>()
 | 
			
		||||
        .register_model::<Sale>()
 | 
			
		||||
        .register_model::<Vote>()
 | 
			
		||||
        .register_model::<Shareholder>()
 | 
			
		||||
        .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::<User>()?;
 | 
			
		||||
    db.register::<Company>()?;
 | 
			
		||||
    db.register::<Meeting>()?;
 | 
			
		||||
    db.register::<Product>()?;
 | 
			
		||||
    db.register::<Sale>()?;
 | 
			
		||||
    db.register::<Vote>()?;
 | 
			
		||||
    db.register::<Shareholder>()?;
 | 
			
		||||
    
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
@@ -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<dyn std::error::Error>> {
 | 
			
		||||
    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::<Product>()
 | 
			
		||||
        .register_model::<Currency>()
 | 
			
		||||
        .register_model::<Sale>()
 | 
			
		||||
        .register_model::<ExchangeRate>()
 | 
			
		||||
        .register_model::<Service>()
 | 
			
		||||
        .register_model::<Customer>()
 | 
			
		||||
        .register_model::<Contract>()
 | 
			
		||||
        .register_model::<Invoice>()
 | 
			
		||||
        .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(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								herodb/src/cmd/dbexample_biz/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								herodb/src/cmd/dbexample_biz/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -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.
 | 
			
		||||
							
								
								
									
										275
									
								
								herodb/src/cmd/dbexample_biz/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								herodb/src/cmd/dbexample_biz/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								herodb/src/cmd/dbexample_biz/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								herodb/src/cmd/dbexample_biz/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
@@ -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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 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<dyn std::error::Error>> {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 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<dyn std::error::Error>> {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // 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<dyn std::error::Error>> {
 | 
			
		||||
 | 
			
		||||
    // 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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
 | 
			
		||||
    // 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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
    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<dyn std::error::Error>> {
 | 
			
		||||
    // 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");
 | 
			
		||||
							
								
								
									
										2161
									
								
								herodb/src/cmd/dbexample_governance/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2161
									
								
								herodb/src/cmd/dbexample_governance/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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"
 | 
			
		||||
							
								
								
									
										360
									
								
								herodb/src/cmd/dbexample_prod/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								herodb/src/cmd/dbexample_prod/main.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<dyn std::error::Error>> {
 | 
			
		||||
    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::<Product>()
 | 
			
		||||
        .build_type::<ProductBuilder>()
 | 
			
		||||
        .build_type::<ProductComponentBuilder>()
 | 
			
		||||
        .build_type::<Currency>()
 | 
			
		||||
        .build_type::<CurrencyBuilder>()
 | 
			
		||||
        .build_type::<Sale>()
 | 
			
		||||
        .build_type::<SaleBuilder>()
 | 
			
		||||
        .build_type::<DBBuilder>()
 | 
			
		||||
        .build_type::<DB>();
 | 
			
		||||
 | 
			
		||||
    // 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::<String>);
 | 
			
		||||
    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::<String>);
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "description",
 | 
			
		||||
        ProductComponentBuilder::description::<String>,
 | 
			
		||||
    );
 | 
			
		||||
    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::<String>);
 | 
			
		||||
    engine.register_fn("description", ProductBuilder::description::<String>);
 | 
			
		||||
    engine.register_fn("price", ProductBuilder::price);
 | 
			
		||||
    engine.register_fn("type", ProductBuilder::type_);
 | 
			
		||||
    engine.register_fn("category", ProductBuilder::category::<String>);
 | 
			
		||||
    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::<String>);
 | 
			
		||||
    engine.register_fn("register_currency", DBBuilder::register_model::<Currency>);
 | 
			
		||||
    engine.register_fn("register_product", DBBuilder::register_model::<Product>);
 | 
			
		||||
    engine.register_fn("register_sale", DBBuilder::register_model::<Sale>);
 | 
			
		||||
    engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
 | 
			
		||||
    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::<Product>()
 | 
			
		||||
        .register_model::<Currency>()
 | 
			
		||||
        .register_model::<Sale>()
 | 
			
		||||
        .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(())
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								herodb/src/cmd/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								herodb/src/cmd/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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;
 | 
			
		||||
@@ -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<Self> {
 | 
			
		||||
        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<T: SledModel> SledDB<T> {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Box<dyn std::any::Any>>;
 | 
			
		||||
    fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
 | 
			
		||||
    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<T: SledModel> AnyDbOperations for SledDB<T> {
 | 
			
		||||
    fn delete(&self, id: &str) -> SledDBResult<()> {
 | 
			
		||||
        self.delete(id)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
 | 
			
		||||
        let result = self.get(id)?;
 | 
			
		||||
        Ok(Box::new(result))
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
 | 
			
		||||
        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::<T>() {
 | 
			
		||||
@@ -51,7 +52,7 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
 | 
			
		||||
            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<TypeId, Box<dyn AnyDbOperations>>,
 | 
			
		||||
    
 | 
			
		||||
    type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>>,
 | 
			
		||||
 | 
			
		||||
    // Locks to ensure thread safety for key areas
 | 
			
		||||
    _write_locks: Arc<Mutex<HashMap<String, bool>>>,
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Transaction state
 | 
			
		||||
    transaction: RwLock<Option<TransactionState>>,
 | 
			
		||||
    transaction: Arc<RwLock<Option<TransactionState>>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Builder for DB that allows registering models
 | 
			
		||||
#[derive(Clone, CustomType)]
 | 
			
		||||
pub struct DBBuilder {
 | 
			
		||||
    base_path: PathBuf,
 | 
			
		||||
    model_registrations: Vec<Box<dyn ModelRegistration>>,
 | 
			
		||||
    model_registrations: Vec<Arc<dyn ModelRegistration>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Trait for model registration
 | 
			
		||||
@@ -129,33 +132,45 @@ impl DBBuilder {
 | 
			
		||||
            model_registrations: Vec::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    pub fn with_path<P: Into<PathBuf>>(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<T: SledModel>(mut self) -> Self {
 | 
			
		||||
        self.model_registrations.push(Box::new(SledModelRegistration::<T>::new()));
 | 
			
		||||
        self.model_registrations
 | 
			
		||||
            .push(Arc::new(SledModelRegistration::<T>::new()));
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /// Build the DB with the registered models
 | 
			
		||||
    pub fn build(self) -> SledDBResult<DB> {
 | 
			
		||||
    pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
 | 
			
		||||
        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<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
 | 
			
		||||
        
 | 
			
		||||
        let mut type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>> = 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<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
 | 
			
		||||
        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<T: SledModel>(&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::<T>(),
 | 
			
		||||
                    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::<T>()) {
 | 
			
		||||
            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<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, 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::<T>();
 | 
			
		||||
            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::<T>(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::<T>()) {
 | 
			
		||||
            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<T: SledModel>(&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::<T>(),
 | 
			
		||||
                    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::<T>()) {
 | 
			
		||||
            Some(db_ops) => db_ops.delete(id),
 | 
			
		||||
            None => Err(SledDBError::TypeError),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /// List all model instances of a specific type
 | 
			
		||||
    pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
 | 
			
		||||
        // 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<T: SledModel>(&mut self) -> SledDBResult<()> {
 | 
			
		||||
        let db_path = self.db_path.join(T::db_prefix());
 | 
			
		||||
        let db: SledDB<T> = SledDB::open(db_path)?;
 | 
			
		||||
        self.type_map.insert(TypeId::of::<T>(), Box::new(db));
 | 
			
		||||
        self.type_map.insert(TypeId::of::<T>(), Arc::new(db));
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    // Get a typed handle to a registered model DB
 | 
			
		||||
    pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
 | 
			
		||||
        match self.type_map.get(&TypeId::of::<T>()) {
 | 
			
		||||
            Some(db) => Ok(&**db),
 | 
			
		||||
            None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::<T>()))),
 | 
			
		||||
            None => Err(SledDBError::GeneralError(format!(
 | 
			
		||||
                "No DB registered for type {}",
 | 
			
		||||
                std::any::type_name::<T>()
 | 
			
		||||
            ))),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,25 +5,27 @@ macro_rules! impl_model_methods {
 | 
			
		||||
        impl DB {
 | 
			
		||||
            paste::paste! {
 | 
			
		||||
                /// Insert a model instance into the database
 | 
			
		||||
                pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
 | 
			
		||||
                    self.set(item)
 | 
			
		||||
                pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
 | 
			
		||||
                    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 [<get_ $singular>](&self, id: u32) -> SledDBResult<$model> {
 | 
			
		||||
                pub fn [<get_ $singular>](&mut self, id: i64) -> SledDBResult<$model> {
 | 
			
		||||
                    self.get::<$model>(&id.to_string())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                /// Delete a model instance by its ID
 | 
			
		||||
                pub fn [<delete_ $singular>](&self, id: u32) -> SledDBResult<()> {
 | 
			
		||||
                pub fn [<delete_ $singular>](&mut self, id: i64) -> SledDBResult<()> {
 | 
			
		||||
                    self.delete::<$model>(&id.to_string())
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                /// List all model instances
 | 
			
		||||
                pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
 | 
			
		||||
                pub fn [<list_ $plural>](&mut self) -> SledDBResult<Vec<$model>> {
 | 
			
		||||
                    self.list::<$model>()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<u32> - List of contact IDs
 | 
			
		||||
- `created_at`: DateTime<Utc> - Creation timestamp
 | 
			
		||||
- `updated_at`: DateTime<Utc> - 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<Utc> - Creation timestamp
 | 
			
		||||
- `updated_at`: DateTime<Utc> - Last update timestamp
 | 
			
		||||
- `max_amount`: u16 - Maximum quantity available
 | 
			
		||||
- `max_amount`: i64 - Maximum quantity available
 | 
			
		||||
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
 | 
			
		||||
- `active_till`: DateTime<Utc> - When product/service expires
 | 
			
		||||
- `components`: Vec<ProductComponent> - 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<Utc> - 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<Utc> - When service started
 | 
			
		||||
- `created_at`: DateTime<Utc> - Creation timestamp
 | 
			
		||||
- `updated_at`: DateTime<Utc> - Last update timestamp
 | 
			
		||||
- `items`: Vec<ServiceItem> - 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<Utc> - 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<u32> - ID of the service created from this sale (to be added)
 | 
			
		||||
- `sale_date`: DateTime<Utc> - When sale occurred
 | 
			
		||||
- `created_at`: DateTime<Utc> - Creation timestamp
 | 
			
		||||
- `updated_at`: DateTime<Utc> - Last update timestamp
 | 
			
		||||
- `items`: Vec<SaleItem> - 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<Utc> - 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<u32> - ID of the service this item is for
 | 
			
		||||
- `sale_id`: Option<u32> - 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<Utc> - When invoice was issued
 | 
			
		||||
- `due_date`: DateTime<Utc> - When payment is due
 | 
			
		||||
- `created_at`: DateTime<Utc> - Creation timestamp
 | 
			
		||||
- `updated_at`: DateTime<Utc> - Last update timestamp
 | 
			
		||||
- `items`: Vec<InvoiceItem> - List of items in the invoice
 | 
			
		||||
- `payments`: Vec<Payment> - 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);
 | 
			
		||||
```
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
        <<enumeration>>
 | 
			
		||||
        Hourly
 | 
			
		||||
        Daily
 | 
			
		||||
        Weekly
 | 
			
		||||
        Monthly
 | 
			
		||||
        Yearly
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class ServiceStatus {
 | 
			
		||||
        <<enumeration>>
 | 
			
		||||
        Active
 | 
			
		||||
        Paused
 | 
			
		||||
        Cancelled
 | 
			
		||||
        Completed
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class ContractStatus {
 | 
			
		||||
        <<enumeration>>
 | 
			
		||||
        Active
 | 
			
		||||
        Expired
 | 
			
		||||
        Terminated
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class InvoiceStatus {
 | 
			
		||||
        <<enumeration>>
 | 
			
		||||
        Draft
 | 
			
		||||
        Sent
 | 
			
		||||
        Paid
 | 
			
		||||
        Overdue
 | 
			
		||||
        Cancelled
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    class PaymentStatus {
 | 
			
		||||
        <<enumeration>>
 | 
			
		||||
        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<Utc>
 | 
			
		||||
    - created_at: DateTime<Utc>
 | 
			
		||||
    - updated_at: DateTime<Utc>
 | 
			
		||||
    - items: Vec<ServiceItem>
 | 
			
		||||
  - 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<Utc>
 | 
			
		||||
  - 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<u32>
 | 
			
		||||
    - created_at: DateTime<Utc>
 | 
			
		||||
    - updated_at: DateTime<Utc>
 | 
			
		||||
  - 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<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>
 | 
			
		||||
  - 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<Utc>
 | 
			
		||||
    - due_date: DateTime<Utc>
 | 
			
		||||
    - created_at: DateTime<Utc>
 | 
			
		||||
    - updated_at: DateTime<Utc>
 | 
			
		||||
    - items: Vec<InvoiceItem>
 | 
			
		||||
    - payments: Vec<Payment>
 | 
			
		||||
  - 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<u32>
 | 
			
		||||
    - sale_id: Option<u32>
 | 
			
		||||
 | 
			
		||||
- **Payment**: Struct for tracking payments
 | 
			
		||||
  - Fields:
 | 
			
		||||
    - amount: Currency
 | 
			
		||||
    - date: DateTime<Utc>
 | 
			
		||||
    - 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
 | 
			
		||||
@@ -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<Currency> {
 | 
			
		||||
        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<Currency> {
 | 
			
		||||
        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<f64>,
 | 
			
		||||
    currency_code: Option<String>,
 | 
			
		||||
@@ -68,7 +53,7 @@ impl CurrencyBuilder {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Build the Currency object
 | 
			
		||||
    pub fn build(self) -> Result<Currency, &'static str> {
 | 
			
		||||
    pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
 | 
			
		||||
        Ok(Currency {
 | 
			
		||||
            amount: self.amount.ok_or("amount is required")?,
 | 
			
		||||
            currency_code: self.currency_code.ok_or("currency_code is required")?,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,15 +27,17 @@ pub struct Payment {
 | 
			
		||||
    pub amount: Currency,
 | 
			
		||||
    pub date: DateTime<Utc>,
 | 
			
		||||
    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,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
    pub energy_usage: f64, // Energy usage in watts
 | 
			
		||||
    pub cost: Currency,    // Cost of the component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ProductComponent {
 | 
			
		||||
    /// Create a new product component with default timestamps
 | 
			
		||||
    pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self {
 | 
			
		||||
    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<u32>,
 | 
			
		||||
    id: Option<i64>,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    quantity: Option<i32>,
 | 
			
		||||
    quantity: Option<i64>,
 | 
			
		||||
    created_at: Option<DateTime<Utc>>,
 | 
			
		||||
    updated_at: Option<DateTime<Utc>>,
 | 
			
		||||
    energy_usage: Option<f64>,
 | 
			
		||||
    cost: Option<Currency>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<ProductComponent, &'static str> {
 | 
			
		||||
    pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
 | 
			
		||||
        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<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
    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<Utc>,
 | 
			
		||||
    pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
 | 
			
		||||
    pub components: Vec<ProductComponent>,
 | 
			
		||||
@@ -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<Utc>) {
 | 
			
		||||
        self.purchase_till = purchase_till;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /// Update the active timeframe
 | 
			
		||||
    pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
 | 
			
		||||
        self.active_till = active_till;
 | 
			
		||||
        self.updated_at = Utc::now();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /// Check if the product is available for purchase
 | 
			
		||||
    pub fn is_purchasable(&self) -> bool {
 | 
			
		||||
        (self.status == ProductStatus::Available || self.status == ProductStatus::Active) 
 | 
			
		||||
            && Utc::now() <= self.purchase_till
 | 
			
		||||
        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<Currency> {
 | 
			
		||||
        // If the price is already in the requested currency, return it
 | 
			
		||||
        if self.price.currency_code == currency_code {
 | 
			
		||||
            return Some(self.price.clone());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Convert the price to the requested currency
 | 
			
		||||
        self.price.to_currency(currency_code)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculate the total cost in USD
 | 
			
		||||
    pub fn cost_in_usd(&self) -> Option<Currency> {
 | 
			
		||||
        self.cost_in_currency("USD")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculate the total energy usage of the product (sum of all components)
 | 
			
		||||
    pub fn total_energy_usage(&self) -> f64 {
 | 
			
		||||
        self.components.iter().map(|c| c.total_energy_usage()).sum()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculate the total cost of all components
 | 
			
		||||
    pub fn components_cost(&self, currency_code: &str) -> Option<Currency> {
 | 
			
		||||
        if self.components.is_empty() {
 | 
			
		||||
            return Some(Currency::new(0.0, currency_code.to_string()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Sum up the costs of all components, converting to the requested currency
 | 
			
		||||
        let mut total = 0.0;
 | 
			
		||||
        for component in &self.components {
 | 
			
		||||
            let component_cost = component.total_cost();
 | 
			
		||||
            if let Some(converted_cost) = component_cost.to_currency(currency_code) {
 | 
			
		||||
                total += converted_cost.amount;
 | 
			
		||||
            } else {
 | 
			
		||||
                return None; // Conversion failed
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Some(Currency::new(total, currency_code.to_string()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Calculate the total cost of all components in USD
 | 
			
		||||
    pub fn components_cost_in_usd(&self) -> Option<Currency> {
 | 
			
		||||
        self.components_cost("USD")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Builder for Product
 | 
			
		||||
#[derive(Clone, CustomType)]
 | 
			
		||||
pub struct ProductBuilder {
 | 
			
		||||
    id: Option<u32>,
 | 
			
		||||
    id: Option<i64>,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    price: Option<Currency>,
 | 
			
		||||
@@ -291,7 +215,7 @@ pub struct ProductBuilder {
 | 
			
		||||
    status: Option<ProductStatus>,
 | 
			
		||||
    created_at: Option<DateTime<Utc>>,
 | 
			
		||||
    updated_at: Option<DateTime<Utc>>,
 | 
			
		||||
    max_amount: Option<u16>,
 | 
			
		||||
    max_amount: Option<i64>,
 | 
			
		||||
    purchase_till: Option<DateTime<Utc>>,
 | 
			
		||||
    active_till: Option<DateTime<Utc>>,
 | 
			
		||||
    components: Vec<ProductComponent>,
 | 
			
		||||
@@ -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;
 | 
			
		||||
use crate::models::biz::Currency;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Utc>, // 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<Utc>,
 | 
			
		||||
    ) -> 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<u32>,
 | 
			
		||||
    sale_id: Option<u32>,
 | 
			
		||||
    product_id: Option<u32>,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    comments: Option<String>,
 | 
			
		||||
    quantity: Option<i32>,
 | 
			
		||||
    unit_price: Option<Currency>,
 | 
			
		||||
    subtotal: Option<Currency>,
 | 
			
		||||
    tax_rate: Option<f64>,
 | 
			
		||||
    tax_amount: Option<Currency>,
 | 
			
		||||
    active_till: Option<DateTime<Utc>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -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<S: Into<String>>(mut self, description: S) -> Self {
 | 
			
		||||
        self.description = Some(description.into());
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Set the comments
 | 
			
		||||
    pub fn comments<S: Into<String>>(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<Utc>) -> Self {
 | 
			
		||||
        self.active_till = Some(active_till);
 | 
			
		||||
@@ -130,36 +184,52 @@ impl SaleItemBuilder {
 | 
			
		||||
    pub fn build(self) -> Result<SaleItem, &'static str> {
 | 
			
		||||
        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<u32>, // ID of the service created from this sale (if applicable)
 | 
			
		||||
    pub sale_date: DateTime<Utc>,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
@@ -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<crate::models::biz::Service, &'static str> {
 | 
			
		||||
        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<u32>,
 | 
			
		||||
    company_id: Option<u32>,
 | 
			
		||||
    customer_id: Option<u32>,
 | 
			
		||||
    buyer_name: Option<String>,
 | 
			
		||||
    buyer_email: Option<String>,
 | 
			
		||||
    subtotal_amount: Option<Currency>,
 | 
			
		||||
    tax_amount: Option<Currency>,
 | 
			
		||||
    total_amount: Option<Currency>,
 | 
			
		||||
    status: Option<SaleStatus>,
 | 
			
		||||
    service_id: Option<u32>,
 | 
			
		||||
    sale_date: Option<DateTime<Utc>>,
 | 
			
		||||
    created_at: Option<DateTime<Utc>>,
 | 
			
		||||
    updated_at: Option<DateTime<Utc>>,
 | 
			
		||||
@@ -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<S: Into<String>>(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<Utc>) -> 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),
 | 
			
		||||
 
 | 
			
		||||
@@ -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<u32>,
 | 
			
		||||
    product_id: Option<u32>,
 | 
			
		||||
    name: Option<String>,
 | 
			
		||||
    description: Option<String>,
 | 
			
		||||
    comments: Option<String>,
 | 
			
		||||
    quantity: Option<i32>,
 | 
			
		||||
    unit_price: Option<Currency>,
 | 
			
		||||
    subtotal: Option<Currency>,
 | 
			
		||||
@@ -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<S: Into<String>>(mut self, description: S) -> Self {
 | 
			
		||||
        self.description = Some(description.into());
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Set the comments
 | 
			
		||||
    pub fn comments<S: Into<String>>(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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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<String>,     // public keys of the member
 | 
			
		||||
    pub emails: Vec<String>,      // 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<Member>,    // 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<String, String> {
 | 
			
		||||
        let mut keys = HashMap::new();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								herodb/src/models/circle/member.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								herodb/src/models/circle/member.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<String>,      // 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<u32>,    // IDs of contacts linked to this member
 | 
			
		||||
    pub wallet_ids: Vec<u32>,     // 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<String, String> {
 | 
			
		||||
        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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								herodb/src/models/circle/wallet.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								herodb/src/models/circle/wallet.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<Asset>,       // 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<String, String> {
 | 
			
		||||
        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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										496
									
								
								herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										496
									
								
								herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md
									
									
									
									
									
										Normal file
									
								
							@@ -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<u32>,
 | 
			
		||||
    pub vote_id: Option<u32>,
 | 
			
		||||
    pub title: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub text: String,
 | 
			
		||||
    pub status: ResolutionStatus,
 | 
			
		||||
    pub proposed_by: u32, // User ID
 | 
			
		||||
    pub proposed_at: DateTime<Utc>,
 | 
			
		||||
    pub approved_at: Option<DateTime<Utc>>,
 | 
			
		||||
    pub rejected_at: Option<DateTime<Utc>>,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
    pub approvals: Vec<Approval>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<Resolution>) -> Result<Vec<Resolution>, 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<Resolution>) -> Result<Vec<Resolution>, 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<Resolution>) -> Result<Option<Resolution>, 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
 | 
			
		||||
@@ -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)]
 | 
			
		||||
@@ -167,8 +167,8 @@ impl Company {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Get all resolutions for this company
 | 
			
		||||
    pub fn get_resolutions(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Vec<crate::models::governance::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list()?;
 | 
			
		||||
    pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list::<super::Resolution>()?;
 | 
			
		||||
        let company_resolutions = all_resolutions
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|resolution| resolution.company_id == self.id)
 | 
			
		||||
							
								
								
									
										212
									
								
								herodb/src/models/gov/compliance.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								herodb/src/models/gov/compliance.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<Utc>,
 | 
			
		||||
    pub status: String,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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<Utc>,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// 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<Utc>,
 | 
			
		||||
    pub end_date: DateTime<Utc>,
 | 
			
		||||
    pub status: String,
 | 
			
		||||
    pub findings: String,
 | 
			
		||||
    pub created_at: DateTime<Utc>,
 | 
			
		||||
    pub updated_at: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<Utc>,
 | 
			
		||||
    ) -> 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<Company>) -> Result<Company, SledDBError> {
 | 
			
		||||
        db.get(&self.company_id.to_string())
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Get all documents associated with this requirement
 | 
			
		||||
    pub fn get_documents(&self, db: &SledDB<ComplianceDocument>) -> Result<Vec<ComplianceDocument>, 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<ComplianceRequirement>) -> Result<ComplianceRequirement, SledDBError> {
 | 
			
		||||
        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<Utc>,
 | 
			
		||||
        end_date: DateTime<Utc>,
 | 
			
		||||
    ) -> 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<Company>) -> Result<Company, SledDBError> {
 | 
			
		||||
        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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -164,8 +164,8 @@ impl Meeting {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Get all resolutions discussed in this meeting
 | 
			
		||||
    pub fn get_resolutions(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Vec<crate::models::governance::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list()?;
 | 
			
		||||
    pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list::<super::Resolution>()?;
 | 
			
		||||
        let meeting_resolutions = all_resolutions
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .filter(|resolution| resolution.meeting_id == Some(self.id))
 | 
			
		||||
@@ -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<Meeting>) -> Result<Option<Meeting>, SledDBError> {
 | 
			
		||||
    pub fn get_meeting(&self, db: &crate::db::DB) -> Result<Option<Meeting>, SledDBError> {
 | 
			
		||||
        match self.meeting_id {
 | 
			
		||||
            Some(meeting_id) => {
 | 
			
		||||
                let meeting = db.get(&meeting_id.to_string())?;
 | 
			
		||||
                let meeting = db.get::<Meeting>(&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<Vote>) -> Result<Option<Vote>, SledDBError> {
 | 
			
		||||
    pub fn get_vote(&self, db: &crate::db::DB) -> Result<Option<Vote>, SledDBError> {
 | 
			
		||||
        match self.vote_id {
 | 
			
		||||
            Some(vote_id) => {
 | 
			
		||||
                let vote = db.get(&vote_id.to_string())?;
 | 
			
		||||
                let vote = db.get::<Vote>(&vote_id.to_string())?;
 | 
			
		||||
                Ok(Some(vote))
 | 
			
		||||
            }
 | 
			
		||||
            None => Ok(None),
 | 
			
		||||
@@ -128,8 +128,8 @@ impl Vote {
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /// Get the resolution associated with this vote
 | 
			
		||||
    pub fn get_resolution(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Option<crate::models::governance::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list()?;
 | 
			
		||||
    pub fn get_resolution(&self, db: &crate::db::DB) -> Result<Option<super::Resolution>, SledDBError> {
 | 
			
		||||
        let all_resolutions = db.list::<super::Resolution>()?;
 | 
			
		||||
        let vote_resolution = all_resolutions
 | 
			
		||||
            .into_iter()
 | 
			
		||||
            .find(|resolution| resolution.vote_id == Some(self.id));
 | 
			
		||||
							
								
								
									
										18
									
								
								herodb/src/models/instructions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								herodb/src/models/instructions.md
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
- 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
pub mod biz;
 | 
			
		||||
pub mod mcc;
 | 
			
		||||
pub mod circle;
 | 
			
		||||
pub mod governance;
 | 
			
		||||
pub mod gov;
 | 
			
		||||
							
								
								
									
										131
									
								
								herodb/src/models/py/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								herodb/src/models/py/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										3
									
								
								herodb/src/models/py/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								herodb/src/models/py/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
"""
 | 
			
		||||
Python port of the business models from Rust.
 | 
			
		||||
"""
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								herodb/src/models/py/__pycache__/api.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								herodb/src/models/py/__pycache__/api.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								herodb/src/models/py/__pycache__/models.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								herodb/src/models/py/__pycache__/models.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										455
									
								
								herodb/src/models/py/api.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										455
									
								
								herodb/src/models/py/api.py
									
									
									
									
									
										Executable file
									
								
							@@ -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)
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								herodb/src/models/py/business.db
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								herodb/src/models/py/business.db
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										190
									
								
								herodb/src/models/py/example.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										190
									
								
								herodb/src/models/py/example.py
									
									
									
									
									
										Executable file
									
								
							@@ -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()
 | 
			
		||||
							
								
								
									
										49
									
								
								herodb/src/models/py/install_and_run.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										49
									
								
								herodb/src/models/py/install_and_run.sh
									
									
									
									
									
										Executable file
									
								
							@@ -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!"
 | 
			
		||||
							
								
								
									
										297
									
								
								herodb/src/models/py/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								herodb/src/models/py/models.py
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
			
		||||
							
								
								
									
										42
									
								
								herodb/src/models/py/server.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										42
									
								
								herodb/src/models/py/server.sh
									
									
									
									
									
										Executable file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										7
									
								
								herodb/src/rhaiengine/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								herodb/src/rhaiengine/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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::*;
 | 
			
		||||
		Reference in New Issue
	
	Block a user