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:
despiegk 2025-04-20 05:41:28 +02:00
commit 838e966dc9
56 changed files with 3972 additions and 3689 deletions

5
herodb/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
target/
temp/
tmp/
*.log
*.tmp

View File

@ -19,7 +19,7 @@ tempfile = "3.8"
poem = "1.3.55"
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
tokio = { version = "1", features = ["full"] }
rhai = "1.15.1"
rhai = "1.21.0"
paste = "1.0"
lazy_static = "1.4.0"
@ -27,10 +27,22 @@ lazy_static = "1.4.0"
name = "rhai_demo"
path = "examples/rhai_demo.rs"
[[example]]
name = "business_models_demo"
path = "examples/business_models_demo.rs"
[[bin]]
name = "dbexample2"
path = "src/cmd/dbexample2/main.rs"
name = "dbexample_prod"
path = "src/cmd/dbexample_prod/main.rs"
[[bin]]
name = "dbexample_mcc"
path = "src/cmd/dbexample_mcc/main.rs"
[[bin]]
name = "dbexample_gov"
path = "src/cmd/dbexample_gov/main.rs"
[[bin]]
name = "dbexample_biz"
path = "src/cmd/dbexample_biz/main.rs"

View File

@ -7,7 +7,12 @@ A database library built on top of sled with model support.
## example
```bash
cargo run --bin dbexample2
#test for mcc module
cargo run --bin dbexample_mcc
#test for governance module
cargo run --bin dbexample_gov
#test for products
cargo run --bin dbexample_prod
```
## Features

View File

@ -0,0 +1,428 @@
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
/// This example demonstrates business models in action:
/// 1. Defining products (2 types of server nodes)
/// 2. Defining components (parts of the nodes)
/// 3. Setting up pricing
/// 4. Creating a function to check which products can be bought
/// 5. Simulating a user buying a product
/// 6. Generating an invoice
/// 7. Simulating payment
fn main() {
println!("Business Models Example");
println!("=======================\n");
// Create a customer
let customer = create_customer();
println!("Created customer: {}", customer.name);
// Define products (server nodes)
let (standard_node, premium_node) = create_server_products();
println!("Created server products:");
println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code);
println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code);
// Check which products can be purchased
println!("\nChecking which products can be purchased:");
let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]);
for product in purchasable_products {
println!(" - {} is available for purchase", product.name);
}
// Simulate a user buying a product
println!("\nSimulating purchase of a Premium Node:");
let sale = create_sale(&customer, &premium_node);
println!(" - Sale created with ID: {}", sale.id);
println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code);
// Generate an invoice
println!("\nGenerating invoice:");
let invoice = create_invoice(&customer, &sale);
println!(" - Invoice created with ID: {}", invoice.id);
println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code);
println!(" - Due date: {}", invoice.due_date);
println!(" - Status: {:?}", invoice.status);
// Simulate payment
println!("\nSimulating payment:");
let paid_invoice = process_payment(invoice);
println!(" - Payment processed");
println!(" - New balance due: ${} {}", paid_invoice.balance_due.amount, paid_invoice.balance_due.currency_code);
println!(" - Payment status: {:?}", paid_invoice.payment_status);
println!(" - Invoice status: {:?}", paid_invoice.status);
println!("\nBusiness transaction completed successfully!");
}
// ===== Model Definitions =====
// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Currency {
amount: f64,
currency_code: String,
}
// Customer represents a customer who can purchase products
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Customer {
id: u32,
name: String,
description: String,
pubkey: String,
}
// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum ProductType {
Product,
Service,
}
// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum ProductStatus {
Available,
Unavailable,
}
// ProductComponent represents a component of a product
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProductComponent {
id: i64,
name: String,
description: String,
quantity: i64,
}
// Product represents a product or service offered
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Product {
id: i64,
name: String,
description: String,
price: Currency,
type_: ProductType,
category: String,
status: ProductStatus,
max_amount: i64,
purchase_till: DateTime<Utc>,
active_till: DateTime<Utc>,
components: Vec<ProductComponent>,
}
// SaleStatus represents the status of a sale
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum SaleStatus {
Pending,
Completed,
Cancelled,
}
// SaleItem represents an item in a sale
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SaleItem {
id: u32,
sale_id: u32,
product_id: u32,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
subtotal: Currency,
tax_rate: f64,
tax_amount: Currency,
active_till: DateTime<Utc>,
}
// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Sale {
id: u32,
company_id: u32,
customer_id: u32,
buyer_name: String,
buyer_email: String,
subtotal_amount: Currency,
tax_amount: Currency,
total_amount: Currency,
status: SaleStatus,
service_id: Option<u32>,
sale_date: DateTime<Utc>,
items: Vec<SaleItem>,
}
// InvoiceStatus represents the status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum InvoiceStatus {
Draft,
Sent,
Paid,
Overdue,
Cancelled,
}
// PaymentStatus represents the payment status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum PaymentStatus {
Unpaid,
PartiallyPaid,
Paid,
}
// Payment represents a payment made against an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Payment {
amount: Currency,
date: DateTime<Utc>,
method: String,
comment: String,
}
// InvoiceItem represents an item in an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InvoiceItem {
id: u32,
invoice_id: u32,
description: String,
amount: Currency,
sale_id: Option<u32>,
}
// Invoice represents an invoice sent to a customer
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Invoice {
id: u32,
customer_id: u32,
total_amount: Currency,
balance_due: Currency,
status: InvoiceStatus,
payment_status: PaymentStatus,
issue_date: DateTime<Utc>,
due_date: DateTime<Utc>,
items: Vec<InvoiceItem>,
payments: Vec<Payment>,
}
// ===== Implementation Functions =====
// Create a customer for our example
fn create_customer() -> Customer {
Customer {
id: 1,
name: "TechCorp Inc.".to_string(),
description: "Enterprise technology company".to_string(),
pubkey: "tech-corp-public-key-123".to_string(),
}
}
// Create two types of server node products with their components
fn create_server_products() -> (Product, Product) {
let now = Utc::now();
// Create currency for pricing
let usd = |amount| {
Currency {
amount,
currency_code: "USD".to_string(),
}
};
// Standard Node Components
let cpu_standard = ProductComponent {
id: 1,
name: "CPU".to_string(),
description: "4-core CPU".to_string(),
quantity: 1,
};
let ram_standard = ProductComponent {
id: 2,
name: "RAM".to_string(),
description: "16GB RAM".to_string(),
quantity: 1,
};
let storage_standard = ProductComponent {
id: 3,
name: "Storage".to_string(),
description: "500GB SSD".to_string(),
quantity: 1,
};
// Premium Node Components
let cpu_premium = ProductComponent {
id: 4,
name: "CPU".to_string(),
description: "8-core CPU".to_string(),
quantity: 1,
};
let ram_premium = ProductComponent {
id: 5,
name: "RAM".to_string(),
description: "32GB RAM".to_string(),
quantity: 1,
};
let storage_premium = ProductComponent {
id: 6,
name: "Storage".to_string(),
description: "1TB SSD".to_string(),
quantity: 1,
};
let gpu_premium = ProductComponent {
id: 7,
name: "GPU".to_string(),
description: "Dedicated GPU".to_string(),
quantity: 1,
};
// Create Standard Node Product
let standard_node = Product {
id: 1,
name: "Standard Server Node".to_string(),
description: "Basic server node for general workloads".to_string(),
price: usd(99.99),
type_: ProductType::Product,
category: "Servers".to_string(),
status: ProductStatus::Available,
max_amount: 100,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(365),
components: vec![cpu_standard, ram_standard, storage_standard],
};
// Create Premium Node Product
let premium_node = Product {
id: 2,
name: "Premium Server Node".to_string(),
description: "High-performance server node for demanding workloads".to_string(),
price: usd(199.99),
type_: ProductType::Product,
category: "Servers".to_string(),
status: ProductStatus::Available,
max_amount: 50,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(365),
components: vec![cpu_premium, ram_premium, storage_premium, gpu_premium],
};
(standard_node, premium_node)
}
// Check which products can be purchased
fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> {
products.iter()
.filter(|p| p.status == ProductStatus::Available && Utc::now() <= p.purchase_till)
.copied()
.collect()
}
// Create a sale for a customer buying a product
fn create_sale(customer: &Customer, product: &Product) -> Sale {
let now = Utc::now();
let active_till = now + Duration::days(365);
// Create a sale item for the product
let sale_item = SaleItem {
id: 1,
sale_id: 1,
product_id: product.id as u32,
name: product.name.clone(),
description: product.description.clone(),
comments: "Customer requested expedited setup".to_string(),
quantity: 1,
unit_price: product.price.clone(),
subtotal: Currency {
amount: product.price.amount * 1.0,
currency_code: product.price.currency_code.clone(),
},
tax_rate: 10.0, // 10% tax rate
tax_amount: Currency {
amount: product.price.amount * 0.1,
currency_code: product.price.currency_code.clone(),
},
active_till,
};
// Calculate totals
let subtotal = sale_item.subtotal.clone();
let tax_amount = sale_item.tax_amount.clone();
let total_amount = Currency {
amount: subtotal.amount + tax_amount.amount,
currency_code: subtotal.currency_code.clone(),
};
// Create the sale
Sale {
id: 1,
company_id: 101, // Assuming company ID 101
customer_id: customer.id,
buyer_name: customer.name.clone(),
buyer_email: "contact@techcorp.com".to_string(), // Example email
subtotal_amount: subtotal,
tax_amount,
total_amount,
status: SaleStatus::Completed,
service_id: None,
sale_date: now,
items: vec![sale_item],
}
}
// Create an invoice for a sale
fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice {
let now = Utc::now();
let due_date = now + Duration::days(30); // Due in 30 days
// Create an invoice item for the sale
let invoice_item = InvoiceItem {
id: 1,
invoice_id: 1,
description: format!("Purchase of {}", sale.items[0].name),
amount: sale.total_amount.clone(),
sale_id: Some(sale.id),
};
// Create the invoice
Invoice {
id: 1,
customer_id: customer.id,
total_amount: sale.total_amount.clone(),
balance_due: sale.total_amount.clone(),
status: InvoiceStatus::Sent,
payment_status: PaymentStatus::Unpaid,
issue_date: now,
due_date,
items: vec![invoice_item],
payments: Vec::new(),
}
}
// Process a payment for an invoice
fn process_payment(mut invoice: Invoice) -> Invoice {
// Create a payment for the full amount
let payment = Payment {
amount: invoice.total_amount.clone(),
date: Utc::now(),
method: "Credit Card".to_string(),
comment: "Payment received via credit card ending in 1234".to_string(),
};
// Add the payment to the invoice
invoice.payments.push(payment);
// Update the balance due
invoice.balance_due.amount = 0.0;
// Update the payment status
invoice.payment_status = PaymentStatus::Paid;
invoice.status = InvoiceStatus::Paid;
invoice
}

View File

@ -1,38 +0,0 @@
//! Demonstrates how to use the Rhai wrappers for our models
use herodb::zaz::rhai::{run_script_file, run_example_script};
use std::path::PathBuf;
use std::fs;
use std::time::SystemTime;
fn main() -> Result<(), String> {
println!("=== RHAI MODEL WRAPPERS DEMONSTRATION ===");
// Run our test script that creates model objects
let test_script_path = "src/zaz/rhai/test.rhai";
println!("\n1. Running model creation test script: {}", test_script_path);
run_script_file(test_script_path)?;
// Create temporary directory for DB example
let temp_dir = create_temp_dir()
.map_err(|e| format!("Failed to create temp dir: {}", e))?;
// Run our example script that uses the DB
println!("\n2. Running example with database at: {:?}", temp_dir);
run_example_script(temp_dir.to_str().unwrap())?;
println!("\n=== DEMONSTRATION COMPLETED SUCCESSFULLY ===");
Ok(())
}
/// Creates a simple temporary directory
fn create_temp_dir() -> std::io::Result<PathBuf> {
let temp_dir = std::env::temp_dir();
let random_name = format!("rhai-demo-{}", SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis());
let path = temp_dir.join(random_name);
fs::create_dir_all(&path)?;
Ok(path)
}

View File

@ -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

View File

@ -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");
}
}

View File

@ -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(())
}

View File

@ -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(())
}

View 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.

View 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
}

View 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;

View File

@ -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");

File diff suppressed because it is too large Load Diff

View File

@ -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"

View 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
View 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;

View File

@ -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
}
}

View File

@ -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>()
))),
}
}
}

View File

@ -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>()
}
}
}
};
}
}

View File

@ -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;

View File

@ -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);
```

View File

@ -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

View File

@ -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")?,

View File

@ -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,
}
}
}

View File

@ -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;

View File

@ -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),

View File

@ -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,

View File

@ -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();

View 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"
}
}

View File

@ -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};

View 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"
}
}

View 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

View File

@ -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)]

View File

@ -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)

View 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"
}
}

View File

@ -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))

View File

@ -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),

View File

@ -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));

View 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)
-

View File

@ -1,4 +1,4 @@
pub mod biz;
pub mod mcc;
pub mod circle;
pub mod governance;
pub mod gov;

View 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

View File

@ -0,0 +1,3 @@
"""
Python port of the business models from Rust.
"""

Binary file not shown.

455
herodb/src/models/py/api.py Executable file
View 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)

Binary file not shown.

190
herodb/src/models/py/example.py Executable file
View 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()

View 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!"

View 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
View 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

View 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::*;