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:
commit
838e966dc9
5
herodb/.gitignore
vendored
Normal file
5
herodb/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
target/
|
||||
temp/
|
||||
tmp/
|
||||
*.log
|
||||
*.tmp
|
@ -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"
|
||||
|
@ -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
|
||||
|
428
herodb/examples/business_models_demo.rs
Normal file
428
herodb/examples/business_models_demo.rs
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
# HeroDB Architecture
|
||||
|
||||
This document explains the architecture of HeroDB, focusing on the separation between model definitions and database logic.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Separation of Concerns**: The DB core should not know about specific models
|
||||
2. **Registration-Based System**: Models get registered with the DB through a factory pattern
|
||||
3. **Type-Safety**: Despite the separation, we maintain full type safety
|
||||
|
||||
## Components
|
||||
|
||||
### Core Module
|
||||
|
||||
The `core` module provides the database foundation without knowing about specific models:
|
||||
|
||||
- `SledModel` trait: Defines the interface models must implement
|
||||
- `Storable` trait: Provides serialization/deserialization capabilities
|
||||
- `SledDB<T>`: Generic database wrapper for any model type
|
||||
- `DB`: Main database manager that holds registered models
|
||||
- `DBBuilder`: Builder for creating a DB with registered models
|
||||
|
||||
### Zaz Module
|
||||
|
||||
The `zaz` module contains domain-specific models and factories:
|
||||
|
||||
- `models`: Defines specific model types like User, Company, etc.
|
||||
- `factory`: Provides functions to create a DB with zaz models registered
|
||||
|
||||
## Using the DB
|
||||
|
||||
### Option 1: Factory Function
|
||||
|
||||
The easiest way to create a DB with all zaz models is to use the factory:
|
||||
|
||||
```rust
|
||||
use herodb::zaz::create_zaz_db;
|
||||
|
||||
// Create a DB with all zaz models registered
|
||||
let db = create_zaz_db("/path/to/db")?;
|
||||
|
||||
// Use the DB with specific model types
|
||||
let user = User::new(...);
|
||||
db.set(&user)?;
|
||||
let retrieved: User = db.get(&id)?;
|
||||
```
|
||||
|
||||
### Option 2: Builder Pattern
|
||||
|
||||
For more control, use the builder pattern to register only the models you need:
|
||||
|
||||
```rust
|
||||
use herodb::core::{DBBuilder, DB};
|
||||
use herodb::zaz::models::{User, Company};
|
||||
|
||||
// Create a DB with only User and Company models
|
||||
let db = DBBuilder::new("/path/to/db")
|
||||
.register_model::<User>()
|
||||
.register_model::<Company>()
|
||||
.build()?;
|
||||
```
|
||||
|
||||
### Option 3: Dynamic Registration
|
||||
|
||||
You can also register models with an existing DB:
|
||||
|
||||
```rust
|
||||
use herodb::core::DB;
|
||||
use herodb::zaz::models::User;
|
||||
|
||||
// Create an empty DB
|
||||
let mut db = DB::new("/path/to/db")?;
|
||||
|
||||
// Register the User model
|
||||
db.register::<User>()?;
|
||||
```
|
||||
|
||||
## Benefits of this Architecture
|
||||
|
||||
1. **Modularity**: The core DB code doesn't need to change when models change
|
||||
2. **Extensibility**: New model types can be added without modifying core DB code
|
||||
3. **Flexibility**: Different modules can define and use their own models with the same DB code
|
||||
4. **Type Safety**: Full compile-time type checking is maintained
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The key to this architecture is the combination of generic types and trait objects:
|
||||
|
||||
- `SledDB<T>` provides type-safe operations for specific model types
|
||||
- `AnyDbOperations` trait allows type-erased operations through a common interface
|
||||
- `TypeId` mapping enables runtime lookup of the correct DB for a given model type
|
@ -1,168 +0,0 @@
|
||||
//! Integration tests for zaz database module
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use sled;
|
||||
use bincode;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Test model for database operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct User {
|
||||
id: u32,
|
||||
name: String,
|
||||
email: String,
|
||||
balance: f64,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
fn new(id: u32, name: String, email: String, balance: f64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
balance,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test basic CRUD operations
|
||||
#[test]
|
||||
fn test_basic_crud() {
|
||||
// Create a temporary directory for testing
|
||||
let temp_dir = tempdir().expect("Failed to create temp directory");
|
||||
println!("Created temporary directory at: {:?}", temp_dir.path());
|
||||
|
||||
// Open a sled database in the temporary directory
|
||||
let db = sled::open(temp_dir.path().join("users")).expect("Failed to open database");
|
||||
println!("Opened database at: {:?}", temp_dir.path().join("users"));
|
||||
|
||||
// CREATE a user
|
||||
let user = User::new(1, "Test User".to_string(), "test@example.com".to_string(), 100.0);
|
||||
let user_key = user.id.to_string();
|
||||
let user_value = bincode::serialize(&user).expect("Failed to serialize user");
|
||||
db.insert(user_key.as_bytes(), user_value).expect("Failed to insert user");
|
||||
db.flush().expect("Failed to flush database");
|
||||
println!("Created user: {} ({})", user.name, user.email);
|
||||
|
||||
// READ the user
|
||||
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||
assert!(result.is_some(), "User should exist");
|
||||
if let Some(data) = result {
|
||||
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||
println!("Retrieved user: {} ({})", retrieved_user.name, retrieved_user.email);
|
||||
assert_eq!(user, retrieved_user, "Retrieved user should match original");
|
||||
}
|
||||
|
||||
// UPDATE the user
|
||||
let updated_user = User::new(1, "Updated User".to_string(), "updated@example.com".to_string(), 150.0);
|
||||
let updated_value = bincode::serialize(&updated_user).expect("Failed to serialize updated user");
|
||||
db.insert(user_key.as_bytes(), updated_value).expect("Failed to update user");
|
||||
db.flush().expect("Failed to flush database");
|
||||
println!("Updated user: {} ({})", updated_user.name, updated_user.email);
|
||||
|
||||
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||
if let Some(data) = result {
|
||||
let retrieved_user: User = bincode::deserialize(&data).expect("Failed to deserialize user");
|
||||
assert_eq!(updated_user, retrieved_user, "Retrieved user should match updated version");
|
||||
} else {
|
||||
panic!("User should exist after update");
|
||||
}
|
||||
|
||||
// DELETE the user
|
||||
db.remove(user_key.as_bytes()).expect("Failed to delete user");
|
||||
db.flush().expect("Failed to flush database");
|
||||
println!("Deleted user");
|
||||
|
||||
let result = db.get(user_key.as_bytes()).expect("Failed to query database");
|
||||
assert!(result.is_none(), "User should be deleted");
|
||||
|
||||
// Clean up
|
||||
drop(db);
|
||||
temp_dir.close().expect("Failed to cleanup temporary directory");
|
||||
}
|
||||
|
||||
/// Test transaction-like behavior with multiple operations
|
||||
#[test]
|
||||
fn test_transaction_behavior() {
|
||||
// Create a temporary directory for testing
|
||||
let temp_dir = tempdir().expect("Failed to create temp directory");
|
||||
println!("Created temporary directory at: {:?}", temp_dir.path());
|
||||
|
||||
// Open a sled database in the temporary directory
|
||||
let db = sled::open(temp_dir.path().join("tx_test")).expect("Failed to open database");
|
||||
println!("Opened transaction test database at: {:?}", temp_dir.path().join("tx_test"));
|
||||
|
||||
// Create initial users
|
||||
let user1 = User::new(1, "User One".to_string(), "one@example.com".to_string(), 100.0);
|
||||
let user2 = User::new(2, "User Two".to_string(), "two@example.com".to_string(), 50.0);
|
||||
|
||||
// Insert initial users
|
||||
db.insert(user1.id.to_string().as_bytes(), bincode::serialize(&user1).unwrap()).unwrap();
|
||||
db.insert(user2.id.to_string().as_bytes(), bincode::serialize(&user2).unwrap()).unwrap();
|
||||
db.flush().unwrap();
|
||||
println!("Inserted initial users");
|
||||
|
||||
// Simulate a transaction - transfer 25.0 from user1 to user2
|
||||
println!("Starting transaction simulation: transfer 25.0 from user1 to user2");
|
||||
|
||||
// Create transaction workspace
|
||||
let mut tx_workspace = HashMap::new();
|
||||
|
||||
// Retrieve current state
|
||||
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
|
||||
let user: User = bincode::deserialize(&data).unwrap();
|
||||
tx_workspace.insert(user1.id.to_string(), user);
|
||||
}
|
||||
|
||||
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
|
||||
let user: User = bincode::deserialize(&data).unwrap();
|
||||
tx_workspace.insert(user2.id.to_string(), user);
|
||||
}
|
||||
|
||||
// Modify both users in the transaction
|
||||
let mut updated_user1 = tx_workspace.get(&user1.id.to_string()).unwrap().clone();
|
||||
let mut updated_user2 = tx_workspace.get(&user2.id.to_string()).unwrap().clone();
|
||||
|
||||
updated_user1.balance -= 25.0;
|
||||
updated_user2.balance += 25.0;
|
||||
|
||||
// Update the workspace
|
||||
tx_workspace.insert(user1.id.to_string(), updated_user1);
|
||||
tx_workspace.insert(user2.id.to_string(), updated_user2);
|
||||
|
||||
// Commit the transaction
|
||||
println!("Committing transaction");
|
||||
for (key, user) in tx_workspace {
|
||||
let user_bytes = bincode::serialize(&user).unwrap();
|
||||
db.insert(key.as_bytes(), user_bytes).unwrap();
|
||||
}
|
||||
db.flush().unwrap();
|
||||
|
||||
// Verify the results
|
||||
if let Some(data) = db.get(user1.id.to_string().as_bytes()).unwrap() {
|
||||
let final_user1: User = bincode::deserialize(&data).unwrap();
|
||||
assert_eq!(final_user1.balance, 75.0, "User1 balance should be 75.0");
|
||||
println!("Verified user1 balance is now {}", final_user1.balance);
|
||||
}
|
||||
|
||||
if let Some(data) = db.get(user2.id.to_string().as_bytes()).unwrap() {
|
||||
let final_user2: User = bincode::deserialize(&data).unwrap();
|
||||
assert_eq!(final_user2.balance, 75.0, "User2 balance should be 75.0");
|
||||
println!("Verified user2 balance is now {}", final_user2.balance);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
drop(db);
|
||||
temp_dir.close().expect("Failed to cleanup temporary directory");
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
//! Factory module for creating a DB with all zaz models registered
|
||||
|
||||
use crate::core::{DB, DBBuilder, SledDBResult};
|
||||
use crate::zaz::models::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Create a new DB instance with all zaz models registered
|
||||
pub fn create_zaz_db<P: Into<PathBuf>>(path: P) -> SledDBResult<DB> {
|
||||
// Using the builder pattern to register all models
|
||||
DBBuilder::new(path)
|
||||
.register_model::<User>()
|
||||
.register_model::<Company>()
|
||||
.register_model::<Meeting>()
|
||||
.register_model::<Product>()
|
||||
.register_model::<Sale>()
|
||||
.register_model::<Vote>()
|
||||
.register_model::<Shareholder>()
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Register all zaz models with an existing DB instance
|
||||
pub fn register_zaz_models(db: &mut DB) -> SledDBResult<()> {
|
||||
// Dynamically register all zaz models
|
||||
db.register::<User>()?;
|
||||
db.register::<Company>()?;
|
||||
db.register::<Meeting>()?;
|
||||
db.register::<Product>()?;
|
||||
db.register::<Sale>()?;
|
||||
db.register::<Vote>()?;
|
||||
db.register::<Shareholder>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,464 +0,0 @@
|
||||
use chrono::{Utc, Duration};
|
||||
use herodb::db::DBBuilder;
|
||||
use herodb::models::biz::{
|
||||
Currency, CurrencyBuilder,
|
||||
Product, ProductBuilder, ProductComponentBuilder,
|
||||
ProductType, ProductStatus,
|
||||
Sale, SaleBuilder, SaleItemBuilder, SaleStatus,
|
||||
ExchangeRate, ExchangeRateBuilder, EXCHANGE_RATE_SERVICE,
|
||||
Service, ServiceBuilder, ServiceItemBuilder, ServiceStatus, BillingFrequency,
|
||||
Customer, CustomerBuilder,
|
||||
Contract, ContractBuilder, ContractStatus,
|
||||
Invoice, InvoiceBuilder, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example 2: Using Builder Pattern and Model-Specific Methods");
|
||||
println!("============================================================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample2");
|
||||
if db_path.exists() {
|
||||
fs::remove_dir_all(&db_path)?;
|
||||
}
|
||||
fs::create_dir_all(&db_path)?;
|
||||
println!("Database path: {:?}", db_path);
|
||||
|
||||
// Create a database instance with our models registered
|
||||
let db = DBBuilder::new(&db_path)
|
||||
.register_model::<Product>()
|
||||
.register_model::<Currency>()
|
||||
.register_model::<Sale>()
|
||||
.register_model::<ExchangeRate>()
|
||||
.register_model::<Service>()
|
||||
.register_model::<Customer>()
|
||||
.register_model::<Contract>()
|
||||
.register_model::<Invoice>()
|
||||
.build()?;
|
||||
|
||||
println!("\n1. Creating Products with Builder Pattern");
|
||||
println!("----------------------------------------");
|
||||
|
||||
// Create a currency using the builder
|
||||
let usd = CurrencyBuilder::new()
|
||||
.amount(0.0) // Initial amount
|
||||
.currency_code("USD")
|
||||
.build()?;
|
||||
|
||||
// Insert the currency
|
||||
db.insert_currency(&usd)?;
|
||||
println!("Currency created: ${} {}", usd.amount, usd.currency_code);
|
||||
|
||||
// Create product components using the builder with energy usage and cost
|
||||
let component1 = ProductComponentBuilder::new()
|
||||
.id(101)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.energy_usage(5.0) // 5 watts
|
||||
.cost(CurrencyBuilder::new()
|
||||
.amount(5.0)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.build()?;
|
||||
|
||||
let component2 = ProductComponentBuilder::new()
|
||||
.id(102)
|
||||
.name("Premium Support")
|
||||
.description("24/7 phone and email support")
|
||||
.quantity(1)
|
||||
.energy_usage(10.0) // 10 watts
|
||||
.cost(CurrencyBuilder::new()
|
||||
.amount(15.0)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.build()?;
|
||||
|
||||
// Create products using the builder
|
||||
let product1 = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Standard Plan")
|
||||
.description("Our standard service offering")
|
||||
.price(CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Active)
|
||||
.max_amount(1000)
|
||||
.validity_days(30)
|
||||
.add_component(component1)
|
||||
.build()?;
|
||||
|
||||
let product2 = ProductBuilder::new()
|
||||
.id(2)
|
||||
.name("Premium Plan")
|
||||
.description("Our premium service offering with priority support")
|
||||
.price(CurrencyBuilder::new()
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Active)
|
||||
.max_amount(500)
|
||||
.validity_days(30)
|
||||
.add_component(component2)
|
||||
.build()?;
|
||||
|
||||
// Insert products using model-specific methods
|
||||
db.insert_product(&product1)?;
|
||||
db.insert_product(&product2)?;
|
||||
|
||||
println!("Product created: {} (${}) USD", product1.name, product1.price.amount);
|
||||
println!("Product created: {} (${}) USD", product2.name, product2.price.amount);
|
||||
|
||||
println!("\n2. Retrieving Products");
|
||||
println!("--------------------");
|
||||
|
||||
// Retrieve products using model-specific methods
|
||||
let retrieved_product1 = db.get_product(1)?;
|
||||
println!("Retrieved: {} (${}) USD", retrieved_product1.name, retrieved_product1.price.amount);
|
||||
println!("Components:");
|
||||
for component in &retrieved_product1.components {
|
||||
println!(" - {} ({}, Energy: {}W, Cost: ${} USD)",
|
||||
component.name,
|
||||
component.description,
|
||||
component.energy_usage,
|
||||
component.cost.amount
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate total energy usage
|
||||
let total_energy = retrieved_product1.total_energy_usage();
|
||||
println!("Total energy usage: {}W", total_energy);
|
||||
|
||||
// Calculate components cost
|
||||
if let Some(components_cost) = retrieved_product1.components_cost_in_usd() {
|
||||
println!("Total components cost: ${} USD", components_cost.amount);
|
||||
}
|
||||
|
||||
println!("\n3. Listing All Products");
|
||||
println!("----------------------");
|
||||
|
||||
// List all products using model-specific methods
|
||||
let all_products = db.list_products()?;
|
||||
println!("Found {} products:", all_products.len());
|
||||
for product in all_products {
|
||||
println!(" - {} (${} USD, {})",
|
||||
product.name,
|
||||
product.price.amount,
|
||||
if product.is_purchasable() { "Available" } else { "Unavailable" }
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n4. Creating a Sale");
|
||||
println!("-----------------");
|
||||
|
||||
// Create a sale using the builder
|
||||
let now = Utc::now();
|
||||
|
||||
let item1 = SaleItemBuilder::new()
|
||||
.id(201)
|
||||
.sale_id(1)
|
||||
.product_id(1)
|
||||
.name("Standard Plan")
|
||||
.quantity(1)
|
||||
.unit_price(CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()?;
|
||||
|
||||
let sale = SaleBuilder::new()
|
||||
.id(1)
|
||||
.company_id(101)
|
||||
.buyer_name("John Doe")
|
||||
.buyer_email("john.doe@example.com")
|
||||
.currency_code("USD")
|
||||
.status(SaleStatus::Pending)
|
||||
.add_item(item1)
|
||||
.build()?;
|
||||
|
||||
// Insert the sale using model-specific methods
|
||||
db.insert_sale(&sale)?;
|
||||
println!("Sale created: #{} for {} (${} USD)",
|
||||
sale.id,
|
||||
sale.buyer_name,
|
||||
sale.total_amount.amount
|
||||
);
|
||||
|
||||
println!("\n5. Updating a Sale");
|
||||
println!("-----------------");
|
||||
|
||||
// Retrieve the sale, update it, and save it back
|
||||
let mut retrieved_sale = db.get_sale(1)?;
|
||||
println!("Retrieved sale: #{} with status {:?}", retrieved_sale.id, retrieved_sale.status);
|
||||
|
||||
// Update the status
|
||||
retrieved_sale.update_status(SaleStatus::Completed);
|
||||
db.insert_sale(&retrieved_sale)?;
|
||||
|
||||
println!("Updated sale status to {:?}", retrieved_sale.status);
|
||||
|
||||
println!("\n6. Working with Exchange Rates");
|
||||
println!("----------------------------");
|
||||
|
||||
// Create and set exchange rates using the builder
|
||||
let eur_rate = ExchangeRateBuilder::new()
|
||||
.base_currency("EUR")
|
||||
.target_currency("USD")
|
||||
.rate(1.18)
|
||||
.build()?;
|
||||
|
||||
let gbp_rate = ExchangeRateBuilder::new()
|
||||
.base_currency("GBP")
|
||||
.target_currency("USD")
|
||||
.rate(1.38)
|
||||
.build()?;
|
||||
|
||||
// Insert exchange rates into the database
|
||||
db.insert_exchange_rate(&eur_rate)?;
|
||||
db.insert_exchange_rate(&gbp_rate)?;
|
||||
|
||||
// Set the exchange rates in the service
|
||||
EXCHANGE_RATE_SERVICE.set_rate(eur_rate.clone());
|
||||
EXCHANGE_RATE_SERVICE.set_rate(gbp_rate.clone());
|
||||
|
||||
println!("Exchange rates set:");
|
||||
println!(" - 1 EUR = {} USD", eur_rate.rate);
|
||||
println!(" - 1 GBP = {} USD", gbp_rate.rate);
|
||||
|
||||
// Create currencies in different denominations
|
||||
let eur_price = CurrencyBuilder::new()
|
||||
.amount(100.0)
|
||||
.currency_code("EUR")
|
||||
.build()?;
|
||||
|
||||
let gbp_price = CurrencyBuilder::new()
|
||||
.amount(85.0)
|
||||
.currency_code("GBP")
|
||||
.build()?;
|
||||
|
||||
// Convert to USD
|
||||
if let Some(eur_in_usd) = eur_price.to_usd() {
|
||||
println!("{} EUR = {} USD", eur_price.amount, eur_in_usd.amount);
|
||||
} else {
|
||||
println!("Could not convert EUR to USD");
|
||||
}
|
||||
|
||||
if let Some(gbp_in_usd) = gbp_price.to_usd() {
|
||||
println!("{} GBP = {} USD", gbp_price.amount, gbp_in_usd.amount);
|
||||
} else {
|
||||
println!("Could not convert GBP to USD");
|
||||
}
|
||||
|
||||
// Convert between currencies
|
||||
if let Some(eur_in_gbp) = eur_price.to_currency("GBP") {
|
||||
println!("{} EUR = {} GBP", eur_price.amount, eur_in_gbp.amount);
|
||||
} else {
|
||||
println!("Could not convert EUR to GBP");
|
||||
}
|
||||
|
||||
// Test product price conversion
|
||||
let retrieved_product2 = db.get_product(2)?;
|
||||
|
||||
if let Some(price_in_eur) = retrieved_product2.cost_in_currency("EUR") {
|
||||
println!("Product '{}' price: ${} USD = {} EUR",
|
||||
retrieved_product2.name,
|
||||
retrieved_product2.price.amount,
|
||||
price_in_eur.amount
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n7. Deleting Objects");
|
||||
println!("------------------");
|
||||
|
||||
// Delete a product
|
||||
db.delete_product(2)?;
|
||||
println!("Deleted product #2");
|
||||
|
||||
// List remaining products
|
||||
let remaining_products = db.list_products()?;
|
||||
println!("Remaining products: {}", remaining_products.len());
|
||||
for product in remaining_products {
|
||||
println!(" - {}", product.name);
|
||||
}
|
||||
|
||||
println!("\n8. Creating a Customer");
|
||||
println!("--------------------");
|
||||
|
||||
// Create a customer using the builder
|
||||
let customer = CustomerBuilder::new()
|
||||
.id(1001)
|
||||
.name("Jane Smith")
|
||||
.description("Enterprise customer")
|
||||
.pubkey("abc123def456")
|
||||
.add_contact(5001)
|
||||
.add_contact(5002)
|
||||
.build()?;
|
||||
|
||||
// Insert the customer
|
||||
db.insert_customer(&customer)?;
|
||||
println!("Customer created: {} (ID: {})", customer.name, customer.id);
|
||||
println!("Contacts: {:?}", customer.contact_ids);
|
||||
|
||||
println!("\n9. Creating a Service");
|
||||
println!("-------------------");
|
||||
|
||||
// Create service items using the builder
|
||||
let service_item1 = ServiceItemBuilder::new()
|
||||
.id(301)
|
||||
.service_id(2001)
|
||||
.product_id(1)
|
||||
.name("Standard Plan - Monthly")
|
||||
.quantity(1)
|
||||
.unit_price(CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.tax_rate(0.07) // 7% tax
|
||||
.is_taxable(true)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()?;
|
||||
|
||||
// Create a service using the builder
|
||||
let service = ServiceBuilder::new()
|
||||
.id(2001)
|
||||
.customer_id(1001)
|
||||
.currency_code("USD")
|
||||
.status(ServiceStatus::Active)
|
||||
.billing_frequency(BillingFrequency::Monthly)
|
||||
.add_item(service_item1)
|
||||
.build()?;
|
||||
|
||||
// Insert the service
|
||||
db.insert_service(&service)?;
|
||||
println!("Service created: #{} for customer #{}", service.id, service.customer_id);
|
||||
println!("Total amount: ${} USD (including tax)", service.total_amount.amount);
|
||||
println!("Billing frequency: {:?}", service.billing_frequency);
|
||||
|
||||
println!("\n10. Creating a Contract");
|
||||
println!("---------------------");
|
||||
|
||||
// Create a contract using the builder
|
||||
let contract = ContractBuilder::new()
|
||||
.id(3001)
|
||||
.customer_id(1001)
|
||||
.service_id(2001)
|
||||
.terms("Monthly service contract with auto-renewal")
|
||||
.start_date(now)
|
||||
.end_date(now + Duration::days(365))
|
||||
.auto_renewal(true)
|
||||
.renewal_terms("Renews automatically for 1 year unless cancelled 30 days prior")
|
||||
.status(ContractStatus::Active)
|
||||
.build()?;
|
||||
|
||||
// Insert the contract
|
||||
db.insert_contract(&contract)?;
|
||||
println!("Contract created: #{} for customer #{}", contract.id, contract.customer_id);
|
||||
println!("Contract period: {} to {}",
|
||||
contract.start_date.format("%Y-%m-%d"),
|
||||
contract.end_date.format("%Y-%m-%d")
|
||||
);
|
||||
println!("Auto-renewal: {}", if contract.auto_renewal { "Yes" } else { "No" });
|
||||
|
||||
println!("\n11. Creating an Invoice");
|
||||
println!("---------------------");
|
||||
|
||||
// Create invoice items using the builder
|
||||
let invoice_item1 = InvoiceItemBuilder::new()
|
||||
.id(401)
|
||||
.invoice_id(4001)
|
||||
.description("Monthly service fee - Standard Plan")
|
||||
.amount(CurrencyBuilder::new()
|
||||
.amount(32.09) // Price with tax
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.service_id(2001)
|
||||
.build()?;
|
||||
|
||||
// Create an invoice using the builder
|
||||
let invoice = InvoiceBuilder::new()
|
||||
.id(4001)
|
||||
.customer_id(1001)
|
||||
.currency_code("USD")
|
||||
.issue_date(now)
|
||||
.due_date(now + Duration::days(15))
|
||||
.add_item(invoice_item1)
|
||||
.build()?;
|
||||
|
||||
// Insert the invoice
|
||||
db.insert_invoice(&invoice)?;
|
||||
println!("Invoice created: #{} for customer #{}", invoice.id, invoice.customer_id);
|
||||
println!("Total amount: ${} USD", invoice.total_amount.amount);
|
||||
println!("Balance due: ${} USD", invoice.balance_due.amount);
|
||||
println!("Status: {:?}, Payment status: {:?}", invoice.status, invoice.payment_status);
|
||||
|
||||
println!("\n12. Processing a Payment");
|
||||
println!("----------------------");
|
||||
|
||||
// Retrieve the invoice, add a payment, and save it back
|
||||
let mut retrieved_invoice = db.get_invoice(4001)?;
|
||||
|
||||
// Create a payment
|
||||
let payment = Payment::new(
|
||||
CurrencyBuilder::new()
|
||||
.amount(32.09)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
"Credit Card".to_string()
|
||||
);
|
||||
|
||||
// Add the payment to the invoice
|
||||
retrieved_invoice.add_payment(payment);
|
||||
|
||||
// Save the updated invoice
|
||||
db.insert_invoice(&retrieved_invoice)?;
|
||||
|
||||
println!("Payment processed for invoice #{}", retrieved_invoice.id);
|
||||
println!("New balance due: ${} USD", retrieved_invoice.balance_due.amount);
|
||||
println!("New status: {:?}, Payment status: {:?}",
|
||||
retrieved_invoice.status,
|
||||
retrieved_invoice.payment_status
|
||||
);
|
||||
|
||||
println!("\n13. Retrieving Related Objects");
|
||||
println!("----------------------------");
|
||||
|
||||
// Retrieve customer and related objects
|
||||
let retrieved_customer = db.get_customer(1001)?;
|
||||
println!("Customer: {} (ID: {})", retrieved_customer.name, retrieved_customer.id);
|
||||
|
||||
// Retrieve service for this customer
|
||||
let retrieved_service = db.get_service(2001)?;
|
||||
println!("Service: #{} with {} items",
|
||||
retrieved_service.id,
|
||||
retrieved_service.items.len()
|
||||
);
|
||||
|
||||
// Retrieve contract for this customer
|
||||
let retrieved_contract = db.get_contract(3001)?;
|
||||
println!("Contract: #{} ({})",
|
||||
retrieved_contract.id,
|
||||
if retrieved_contract.is_active() { "Active" } else { "Inactive" }
|
||||
);
|
||||
|
||||
// Retrieve invoice for this customer
|
||||
let retrieved_invoice = db.get_invoice(4001)?;
|
||||
println!("Invoice: #{} ({})",
|
||||
retrieved_invoice.id,
|
||||
match retrieved_invoice.payment_status {
|
||||
PaymentStatus::Paid => "Paid",
|
||||
PaymentStatus::PartiallyPaid => "Partially Paid",
|
||||
PaymentStatus::Unpaid => "Unpaid",
|
||||
}
|
||||
);
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
48
herodb/src/cmd/dbexample_biz/README.md
Normal file
48
herodb/src/cmd/dbexample_biz/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Business Models Example
|
||||
|
||||
This example demonstrates the business models in HeroDB, showcasing a complete business transaction flow from product definition to payment processing.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
1. **Product Definition**: Creating two types of server node products with different components and pricing
|
||||
2. **Component Definition**: Defining the parts that make up each server node (CPU, RAM, Storage, GPU)
|
||||
3. **Pricing Setup**: Setting up prices for products using the Currency model
|
||||
4. **Product Availability**: Checking which products can be purchased based on their status and availability
|
||||
5. **Sales Process**: Simulating a customer purchasing a product
|
||||
6. **Invoice Generation**: Creating an invoice for the sale
|
||||
7. **Payment Processing**: Processing a payment for the invoice and updating its status
|
||||
|
||||
## Business Flow
|
||||
|
||||
The example follows this business flow:
|
||||
|
||||
```
|
||||
Define Products → Check Availability → Customer Purchase → Generate Invoice → Process Payment
|
||||
```
|
||||
|
||||
## Models Used
|
||||
|
||||
- **Product & ProductComponent**: For defining server nodes and their components
|
||||
- **Customer**: For representing the buyer
|
||||
- **Sale & SaleItem**: For recording the purchase transaction
|
||||
- **Invoice & InvoiceItem**: For billing the customer
|
||||
- **Payment**: For recording the payment
|
||||
|
||||
## Running the Example
|
||||
|
||||
To run this example, use:
|
||||
|
||||
```bash
|
||||
cargo run --bin dbexample_biz
|
||||
```
|
||||
|
||||
The output will show each step of the business process with relevant details.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Builder Pattern**: All models use builders for flexible object creation
|
||||
- **Status Tracking**: Sales and invoices have status enums to track their state
|
||||
- **Relationship Modeling**: The example shows how different business entities relate to each other
|
||||
- **Financial Calculations**: Demonstrates tax and total calculations
|
||||
|
||||
This example provides a template for implementing business logic in your own applications using HeroDB.
|
275
herodb/src/cmd/dbexample_biz/main.rs
Normal file
275
herodb/src/cmd/dbexample_biz/main.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use crate::models::biz::{
|
||||
Currency, CurrencyBuilder,
|
||||
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
|
||||
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
|
||||
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, Payment, PaymentStatus,
|
||||
Customer, CustomerBuilder,
|
||||
};
|
||||
use crate::db::base::SledModel;
|
||||
|
||||
/// This example demonstrates the business models in action:
|
||||
/// 1. Defining products (2 types of server nodes)
|
||||
/// 2. Defining components (parts of the nodes)
|
||||
/// 3. Setting up pricing
|
||||
/// 4. Creating a function to check which products can be bought
|
||||
/// 5. Simulating a user buying a product
|
||||
/// 6. Generating an invoice
|
||||
/// 7. Simulating payment
|
||||
|
||||
fn main() {
|
||||
println!("Business Models Example");
|
||||
println!("=======================\n");
|
||||
|
||||
// Create a customer
|
||||
let customer = create_customer();
|
||||
println!("Created customer: {}", customer.name);
|
||||
|
||||
// Define products (server nodes)
|
||||
let (standard_node, premium_node) = create_server_products();
|
||||
println!("Created server products:");
|
||||
println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code);
|
||||
println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code);
|
||||
|
||||
// Check which products can be purchased
|
||||
println!("\nChecking which products can be purchased:");
|
||||
let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]);
|
||||
for product in purchasable_products {
|
||||
println!(" - {} is available for purchase", product.name);
|
||||
}
|
||||
|
||||
// Simulate a user buying a product
|
||||
println!("\nSimulating purchase of a Premium Node:");
|
||||
let sale = create_sale(&customer, &premium_node);
|
||||
println!(" - Sale created with ID: {}", sale.id);
|
||||
println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code);
|
||||
|
||||
// Generate an invoice
|
||||
println!("\nGenerating invoice:");
|
||||
let invoice = create_invoice(&customer, &sale);
|
||||
println!(" - Invoice created with ID: {}", invoice.id);
|
||||
println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code);
|
||||
println!(" - Due date: {}", invoice.due_date);
|
||||
println!(" - Status: {:?}", invoice.status);
|
||||
|
||||
// Simulate payment
|
||||
println!("\nSimulating payment:");
|
||||
let paid_invoice = process_payment(invoice);
|
||||
println!(" - Payment processed");
|
||||
println!(" - New balance due: ${} {}", paid_invoice.balance_due.amount, paid_invoice.balance_due.currency_code);
|
||||
println!(" - Payment status: {:?}", paid_invoice.payment_status);
|
||||
println!(" - Invoice status: {:?}", paid_invoice.status);
|
||||
|
||||
println!("\nBusiness transaction completed successfully!");
|
||||
}
|
||||
|
||||
/// Create a customer for our example
|
||||
fn create_customer() -> Customer {
|
||||
CustomerBuilder::new()
|
||||
.id(1)
|
||||
.name("TechCorp Inc.")
|
||||
.description("Enterprise technology company")
|
||||
.pubkey("tech-corp-public-key-123")
|
||||
.build()
|
||||
.expect("Failed to create customer")
|
||||
}
|
||||
|
||||
/// Create two types of server node products with their components
|
||||
fn create_server_products() -> (Product, Product) {
|
||||
// Create currency for pricing
|
||||
let usd = |amount| {
|
||||
CurrencyBuilder::new()
|
||||
.amount(amount)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to create currency")
|
||||
};
|
||||
|
||||
// Standard Node Components
|
||||
let cpu_standard = ProductComponentBuilder::new()
|
||||
.id(1)
|
||||
.name("CPU")
|
||||
.description("4-core CPU")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create CPU component");
|
||||
|
||||
let ram_standard = ProductComponentBuilder::new()
|
||||
.id(2)
|
||||
.name("RAM")
|
||||
.description("16GB RAM")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create RAM component");
|
||||
|
||||
let storage_standard = ProductComponentBuilder::new()
|
||||
.id(3)
|
||||
.name("Storage")
|
||||
.description("500GB SSD")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create Storage component");
|
||||
|
||||
// Premium Node Components
|
||||
let cpu_premium = ProductComponentBuilder::new()
|
||||
.id(4)
|
||||
.name("CPU")
|
||||
.description("8-core CPU")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create CPU component");
|
||||
|
||||
let ram_premium = ProductComponentBuilder::new()
|
||||
.id(5)
|
||||
.name("RAM")
|
||||
.description("32GB RAM")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create RAM component");
|
||||
|
||||
let storage_premium = ProductComponentBuilder::new()
|
||||
.id(6)
|
||||
.name("Storage")
|
||||
.description("1TB SSD")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create Storage component");
|
||||
|
||||
let gpu_premium = ProductComponentBuilder::new()
|
||||
.id(7)
|
||||
.name("GPU")
|
||||
.description("Dedicated GPU")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to create GPU component");
|
||||
|
||||
// Create Standard Node Product
|
||||
let standard_node = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Standard Server Node")
|
||||
.description("Basic server node for general workloads")
|
||||
.price(usd(99.99))
|
||||
.type_(ProductType::Product)
|
||||
.category("Servers")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(365)
|
||||
.add_component(cpu_standard)
|
||||
.add_component(ram_standard)
|
||||
.add_component(storage_standard)
|
||||
.build()
|
||||
.expect("Failed to create Standard Node product");
|
||||
|
||||
// Create Premium Node Product
|
||||
let premium_node = ProductBuilder::new()
|
||||
.id(2)
|
||||
.name("Premium Server Node")
|
||||
.description("High-performance server node for demanding workloads")
|
||||
.price(usd(199.99))
|
||||
.type_(ProductType::Product)
|
||||
.category("Servers")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(50)
|
||||
.validity_days(365)
|
||||
.add_component(cpu_premium)
|
||||
.add_component(ram_premium)
|
||||
.add_component(storage_premium)
|
||||
.add_component(gpu_premium)
|
||||
.build()
|
||||
.expect("Failed to create Premium Node product");
|
||||
|
||||
(standard_node, premium_node)
|
||||
}
|
||||
|
||||
/// Check which products can be purchased
|
||||
fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> {
|
||||
products.iter()
|
||||
.filter(|p| p.is_purchasable())
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Create a sale for a customer buying a product
|
||||
fn create_sale(customer: &Customer, product: &Product) -> Sale {
|
||||
let now = Utc::now();
|
||||
let active_till = now + Duration::days(365);
|
||||
|
||||
// Create a sale item for the product
|
||||
let sale_item = SaleItemBuilder::new()
|
||||
.id(1)
|
||||
.sale_id(1)
|
||||
.product_id(product.id as u32)
|
||||
.name(product.name.clone())
|
||||
.description(product.description.clone())
|
||||
.comments("Customer requested expedited setup")
|
||||
.quantity(1)
|
||||
.unit_price(product.price.clone())
|
||||
.tax_rate(10.0) // 10% tax rate
|
||||
.active_till(active_till)
|
||||
.build()
|
||||
.expect("Failed to create sale item");
|
||||
|
||||
// Create the sale
|
||||
let sale = SaleBuilder::new()
|
||||
.id(1)
|
||||
.company_id(101) // Assuming company ID 101
|
||||
.customer_id(customer.id)
|
||||
.buyer_name(customer.name.clone())
|
||||
.buyer_email("contact@techcorp.com") // Example email
|
||||
.currency_code(product.price.currency_code.clone())
|
||||
.status(SaleStatus::Completed)
|
||||
.add_item(sale_item)
|
||||
.build()
|
||||
.expect("Failed to create sale");
|
||||
|
||||
sale
|
||||
}
|
||||
|
||||
/// Create an invoice for a sale
|
||||
fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice {
|
||||
let now = Utc::now();
|
||||
let due_date = now + Duration::days(30); // Due in 30 days
|
||||
|
||||
// Create an invoice item for the sale
|
||||
let invoice_item = InvoiceItemBuilder::new()
|
||||
.id(1)
|
||||
.invoice_id(1)
|
||||
.description(format!("Purchase of {}", sale.items[0].name))
|
||||
.amount(sale.total_amount.clone())
|
||||
.sale_id(sale.id)
|
||||
.build()
|
||||
.expect("Failed to create invoice item");
|
||||
|
||||
// Create the invoice
|
||||
let invoice = InvoiceBuilder::new()
|
||||
.id(1)
|
||||
.customer_id(customer.id)
|
||||
.currency_code(sale.total_amount.currency_code.clone())
|
||||
.status(InvoiceStatus::Sent)
|
||||
.issue_date(now)
|
||||
.due_date(due_date)
|
||||
.add_item(invoice_item)
|
||||
.build()
|
||||
.expect("Failed to create invoice");
|
||||
|
||||
invoice
|
||||
}
|
||||
|
||||
/// Process a payment for an invoice
|
||||
fn process_payment(mut invoice: Invoice) -> Invoice {
|
||||
// Create a payment for the full amount
|
||||
let payment = Payment::new(
|
||||
invoice.total_amount.clone(),
|
||||
"Credit Card".to_string(),
|
||||
"Payment received via credit card ending in 1234".to_string()
|
||||
);
|
||||
|
||||
// Add the payment to the invoice
|
||||
invoice.add_payment(payment);
|
||||
|
||||
// The invoice should now be marked as paid
|
||||
assert_eq!(invoice.payment_status, PaymentStatus::Paid);
|
||||
assert_eq!(invoice.status, InvoiceStatus::Paid);
|
||||
|
||||
invoice
|
||||
}
|
10
herodb/src/cmd/dbexample_biz/mod.rs
Normal file
10
herodb/src/cmd/dbexample_biz/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
//! Business example for HeroDB
|
||||
//!
|
||||
//! This module demonstrates business models in action,
|
||||
//! including products, sales, invoices, and payments.
|
||||
|
||||
// Re-export the main function
|
||||
pub use self::main::*;
|
||||
|
||||
// Include the main module
|
||||
mod main;
|
@ -1,6 +1,6 @@
|
||||
use chrono::{Utc, Duration};
|
||||
use herodb::db::DBBuilder;
|
||||
use herodb::models::governance::{
|
||||
use herodb::db::{DBBuilder, SledDB, SledModel};
|
||||
use herodb::models::gov::{
|
||||
Company, CompanyStatus, BusinessType,
|
||||
Shareholder, ShareholderType,
|
||||
Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus,
|
||||
@ -12,11 +12,11 @@ use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example: Governance Module");
|
||||
println!("DB Example: Gov Module");
|
||||
println!("============================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample_governance");
|
||||
let db_path = PathBuf::from("/tmp/dbexample_gov");
|
||||
if db_path.exists() {
|
||||
fs::remove_dir_all(&db_path)?;
|
||||
}
|
||||
@ -58,7 +58,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
|
||||
// Insert the company
|
||||
db.insert(&company)?;
|
||||
db.set(&company)?;
|
||||
println!("Company created: {} (ID: {})", company.name, company.id);
|
||||
println!("Status: {:?}, Business Type: {}", company.status, company.business_type.as_str());
|
||||
|
||||
@ -94,9 +94,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
|
||||
// Insert the users
|
||||
db.insert(&user1)?;
|
||||
db.insert(&user2)?;
|
||||
db.insert(&user3)?;
|
||||
db.set(&user1)?;
|
||||
db.set(&user2)?;
|
||||
db.set(&user3)?;
|
||||
|
||||
println!("User created: {} ({})", user1.name, user1.role);
|
||||
println!("User created: {} ({})", user2.name, user2.role);
|
||||
@ -137,9 +137,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
|
||||
// Insert the shareholders
|
||||
db.insert(&shareholder1)?;
|
||||
db.insert(&shareholder2)?;
|
||||
db.insert(&shareholder3)?;
|
||||
db.set(&shareholder1)?;
|
||||
db.set(&shareholder2)?;
|
||||
db.set(&shareholder3)?;
|
||||
|
||||
println!("Shareholder created: {} ({} shares, {}%)",
|
||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||
@ -150,7 +150,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Update shareholder shares
|
||||
shareholder1.update_shares(1100.0, 44.0);
|
||||
db.insert(&shareholder1)?;
|
||||
db.set(&shareholder1)?;
|
||||
println!("Updated shareholder: {} ({} shares, {}%)",
|
||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||
|
||||
@ -198,7 +198,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
meeting.add_attendee(attendee3);
|
||||
|
||||
// Insert the meeting
|
||||
db.insert(&meeting)?;
|
||||
db.set(&meeting)?;
|
||||
println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M"));
|
||||
println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len());
|
||||
|
||||
@ -209,7 +209,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) {
|
||||
attendee.update_status(AttendeeStatus::Confirmed);
|
||||
}
|
||||
db.insert(&meeting)?;
|
||||
db.set(&meeting)?;
|
||||
|
||||
// Get confirmed attendees
|
||||
let confirmed = meeting.confirmed_attendees();
|
||||
@ -242,19 +242,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
resolution.link_to_meeting(meeting.id);
|
||||
|
||||
// Insert the resolution
|
||||
db.insert(&resolution)?;
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status);
|
||||
|
||||
// Propose the resolution
|
||||
resolution.propose();
|
||||
db.insert(&resolution)?;
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d"));
|
||||
|
||||
// Add approvals
|
||||
resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string());
|
||||
resolution.add_approval(user2.id, user2.name.clone(), true, "Financials look good".to_string());
|
||||
resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string());
|
||||
db.insert(&resolution)?;
|
||||
db.set(&resolution)?;
|
||||
|
||||
// Check approval status
|
||||
println!("Approvals: {}, Rejections: {}",
|
||||
@ -263,7 +263,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Approve the resolution
|
||||
resolution.approve();
|
||||
db.insert(&resolution)?;
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution approved on {}",
|
||||
resolution.approved_at.unwrap().format("%Y-%m-%d"));
|
||||
|
||||
@ -287,7 +287,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
vote.add_option("Abstain".to_string(), 0);
|
||||
|
||||
// Insert the vote
|
||||
db.insert(&vote)?;
|
||||
db.set(&vote)?;
|
||||
println!("Vote created: {} (Status: {:?})", vote.title, vote.status);
|
||||
println!("Voting period: {} to {}",
|
||||
vote.start_date.format("%Y-%m-%d"),
|
||||
@ -297,7 +297,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
vote.add_ballot(user1.id, 1, 1000); // User 1 votes "Approve" with 1000 shares
|
||||
vote.add_ballot(user2.id, 1, 750); // User 2 votes "Approve" with 750 shares
|
||||
vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares
|
||||
db.insert(&vote)?;
|
||||
db.set(&vote)?;
|
||||
|
||||
// Check voting results
|
||||
println!("Voting results:");
|
||||
@ -318,7 +318,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Link the resolution to the vote
|
||||
vote_resolution.link_to_vote(vote.id);
|
||||
vote_resolution.propose();
|
||||
db.insert(&vote_resolution)?;
|
||||
db.set(&vote_resolution)?;
|
||||
println!("Created resolution linked to vote: {}", vote_resolution.title);
|
||||
|
||||
println!("\n7. Retrieving Related Objects");
|
2161
herodb/src/cmd/dbexample_governance/Cargo.lock
generated
2161
herodb/src/cmd/dbexample_governance/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "dbexample_governance"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "dbexample_governance"
|
||||
path = "main.rs"
|
||||
|
||||
[dependencies]
|
||||
herodb = { path = "../../.." }
|
||||
chrono = "0.4"
|
360
herodb/src/cmd/dbexample_prod/main.rs
Normal file
360
herodb/src/cmd/dbexample_prod/main.rs
Normal file
@ -0,0 +1,360 @@
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use herodb::db::{DB, DBBuilder};
|
||||
use herodb::models::biz::{
|
||||
Currency, CurrencyBuilder, Product, ProductBuilder, ProductComponent, ProductComponentBuilder,
|
||||
ProductStatus, ProductType, Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
|
||||
};
|
||||
use rhai::{Engine, packages::Package};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example 2: Using Builder Pattern and Model-Specific Methods");
|
||||
println!("============================================================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample_prod");
|
||||
if db_path.exists() {
|
||||
fs::remove_dir_all(&db_path)?;
|
||||
}
|
||||
fs::create_dir_all(&db_path)?;
|
||||
println!("Database path: {:?}", db_path);
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.build_type::<Product>()
|
||||
.build_type::<ProductBuilder>()
|
||||
.build_type::<ProductComponentBuilder>()
|
||||
.build_type::<Currency>()
|
||||
.build_type::<CurrencyBuilder>()
|
||||
.build_type::<Sale>()
|
||||
.build_type::<SaleBuilder>()
|
||||
.build_type::<DBBuilder>()
|
||||
.build_type::<DB>();
|
||||
|
||||
// Register currency builder methods
|
||||
engine.register_fn("new_currency_builder", CurrencyBuilder::new);
|
||||
engine.register_fn("amount", CurrencyBuilder::amount);
|
||||
engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
|
||||
engine.register_fn("build", CurrencyBuilder::build);
|
||||
|
||||
// Register method to verify currency
|
||||
engine.register_fn("amount", Currency::amount);
|
||||
|
||||
// Register product component builder methods
|
||||
engine.register_fn(
|
||||
"new_product_component_builder",
|
||||
ProductComponentBuilder::new,
|
||||
);
|
||||
engine.register_fn("id", ProductComponentBuilder::id);
|
||||
engine.register_fn("name", ProductComponentBuilder::name::<String>);
|
||||
engine.register_fn(
|
||||
"description",
|
||||
ProductComponentBuilder::description::<String>,
|
||||
);
|
||||
engine.register_fn("quantity", ProductComponentBuilder::quantity);
|
||||
engine.register_fn("build", ProductComponentBuilder::build);
|
||||
|
||||
// Register product builder methods
|
||||
engine.register_fn("new_product_builder", ProductBuilder::new);
|
||||
engine.register_fn("id", ProductBuilder::id);
|
||||
engine.register_fn("name", ProductBuilder::name::<String>);
|
||||
engine.register_fn("description", ProductBuilder::description::<String>);
|
||||
engine.register_fn("price", ProductBuilder::price);
|
||||
engine.register_fn("type", ProductBuilder::type_);
|
||||
engine.register_fn("category", ProductBuilder::category::<String>);
|
||||
engine.register_fn("status", ProductBuilder::status);
|
||||
engine.register_fn("max_amount", ProductBuilder::max_amount);
|
||||
engine.register_fn("validity_days", ProductBuilder::validity_days);
|
||||
engine.register_fn("add_component", ProductBuilder::add_component);
|
||||
engine.register_fn("build", ProductBuilder::build);
|
||||
|
||||
// Register db builder methods
|
||||
engine.register_fn("new_db_builder", DBBuilder::new::<String>);
|
||||
engine.register_fn("register_currency", DBBuilder::register_model::<Currency>);
|
||||
engine.register_fn("register_product", DBBuilder::register_model::<Product>);
|
||||
engine.register_fn("register_sale", DBBuilder::register_model::<Sale>);
|
||||
engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
|
||||
engine.register_fn("build", DBBuilder::build);
|
||||
|
||||
// Register db methods
|
||||
engine.register_fn("insert_currency", DB::insert_currency);
|
||||
engine.register_fn("insert_product", DB::insert_product);
|
||||
|
||||
let script = r#"
|
||||
let usd = new_currency_builder()
|
||||
.amount(0.0)
|
||||
.currency_code("USD")
|
||||
.build();
|
||||
|
||||
// Can we access and print this from the actual Currency?
|
||||
print(usd.amount());
|
||||
|
||||
let db = new_db_builder("./tmp/dbexample2")
|
||||
.register_product()
|
||||
.register_currency()
|
||||
.register_sale()
|
||||
.build();
|
||||
|
||||
db.insert_currency(usd);
|
||||
|
||||
let component1 = new_product_component_builder()
|
||||
.id(101)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.build();
|
||||
|
||||
let component2 = new_product_component_builder()
|
||||
.id(102)
|
||||
.name("Premium Support")
|
||||
.description("24/7 phone and email support")
|
||||
.quantity(1)
|
||||
.build();
|
||||
|
||||
// Create products using the builder
|
||||
// let product1 = new_product_builder()
|
||||
// .id(1)
|
||||
// .name("Standard Plan")
|
||||
// .description("Our standard service offering")
|
||||
// .price(
|
||||
// new_currency_builder()
|
||||
// .amount(29.99)
|
||||
// .currency_code("USD")
|
||||
// .build()
|
||||
// )
|
||||
// .type_(ProductType::Service)
|
||||
// .category("Subscription")
|
||||
// .status(ProductStatus::Available)
|
||||
// .max_amount(1000)
|
||||
// .validity_days(30)
|
||||
// .add_component(component1)
|
||||
// .build();
|
||||
//
|
||||
// let product2 = new_product_builder()
|
||||
// .id(2)
|
||||
// .name("Premium Plan")
|
||||
// .description("Our premium service offering with priority support")
|
||||
// .price(
|
||||
// new_currency_builder()
|
||||
// .amount(99.99)
|
||||
// .currency_code("USD")
|
||||
// .build()
|
||||
// )
|
||||
// .type_(ProductType::Service)
|
||||
// .category("Subscription")
|
||||
// .status(ProductStatus::Available)
|
||||
// .max_amount(500)
|
||||
// .validity_days(30)
|
||||
// .add_component(component2)
|
||||
// .build();
|
||||
|
||||
// Insert products using model-specific methods
|
||||
// db.insert_product(product1);
|
||||
// db.insert_product(product2);
|
||||
"#;
|
||||
|
||||
|
||||
println!("\n0. Executing Script");
|
||||
println!("----------------------------------------");
|
||||
|
||||
|
||||
engine.eval::<()>(script)?;
|
||||
|
||||
// Create a database instance with our models registered
|
||||
let mut db = DBBuilder::new(&db_path)
|
||||
.register_model::<Product>()
|
||||
.register_model::<Currency>()
|
||||
.register_model::<Sale>()
|
||||
.build()?;
|
||||
|
||||
// Check if the currency created in the script is actually present, if it is this value should
|
||||
// be 1 (NOTE: it will be :) ).
|
||||
let currencies = db.list_currencies()?;
|
||||
println!("Found {} currencies in db", currencies.len());
|
||||
for currency in currencies {
|
||||
println!("{} {}", currency.amount, currency.currency_code);
|
||||
}
|
||||
|
||||
println!("\n1. Creating Products with Builder Pattern");
|
||||
println!("----------------------------------------");
|
||||
|
||||
// // Create a currency using the builder
|
||||
// let usd = CurrencyBuilder::new()
|
||||
// .amount(0.0) // Initial amount
|
||||
// .currency_code("USD")
|
||||
// .build()?;
|
||||
//
|
||||
// // Insert the currency
|
||||
// db.insert_currency(usd.clone())?;
|
||||
// println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code);
|
||||
|
||||
// Create product components using the builder
|
||||
let component1 = ProductComponentBuilder::new()
|
||||
.id(101)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.build()?;
|
||||
|
||||
let component2 = ProductComponentBuilder::new()
|
||||
.id(102)
|
||||
.name("Premium Support")
|
||||
.description("24/7 phone and email support")
|
||||
.quantity(1)
|
||||
.build()?;
|
||||
// Create products using the builder
|
||||
let product1 = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Standard Plan")
|
||||
.description("Our standard service offering")
|
||||
.price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(1000)
|
||||
.validity_days(30)
|
||||
.add_component(component1)
|
||||
.build()?;
|
||||
|
||||
let product2 = ProductBuilder::new()
|
||||
.id(2)
|
||||
.name("Premium Plan")
|
||||
.description("Our premium service offering with priority support")
|
||||
.price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(500)
|
||||
.validity_days(30)
|
||||
.add_component(component2)
|
||||
.build()?;
|
||||
|
||||
// Insert products using model-specific methods
|
||||
db.insert_product(product1.clone())?;
|
||||
db.insert_product(product2.clone())?;
|
||||
|
||||
println!(
|
||||
"Product created: {} (${:.2})",
|
||||
product1.name, product1.price.amount
|
||||
);
|
||||
println!(
|
||||
"Product created: {} (${:.2})",
|
||||
product2.name, product2.price.amount
|
||||
);
|
||||
|
||||
println!("\n2. Retrieving Products");
|
||||
println!("--------------------");
|
||||
|
||||
// Retrieve products using model-specific methods
|
||||
let retrieved_product1 = db.get_product(1)?;
|
||||
println!(
|
||||
"Retrieved: {} (${:.2})",
|
||||
retrieved_product1.name, retrieved_product1.price.amount
|
||||
);
|
||||
println!("Components:");
|
||||
for component in &retrieved_product1.components {
|
||||
println!(" - {} ({})", component.name, component.description);
|
||||
}
|
||||
|
||||
println!("\n3. Listing All Products");
|
||||
println!("----------------------");
|
||||
|
||||
// List all products using model-specific methods
|
||||
let all_products = db.list_products()?;
|
||||
println!("Found {} products:", all_products.len());
|
||||
for product in all_products {
|
||||
println!(
|
||||
" - {} (${:.2}, {})",
|
||||
product.name,
|
||||
product.price.amount,
|
||||
if product.is_purchasable() {
|
||||
"Available"
|
||||
} else {
|
||||
"Unavailable"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n4. Creating a Sale");
|
||||
println!("-----------------");
|
||||
|
||||
// Create a sale using the builder
|
||||
let now = Utc::now();
|
||||
|
||||
let item1 = SaleItemBuilder::new()
|
||||
.id(201)
|
||||
.sale_id(1)
|
||||
.product_id(1)
|
||||
.name("Standard Plan")
|
||||
.quantity(1)
|
||||
.unit_price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()?;
|
||||
|
||||
let sale = SaleBuilder::new()
|
||||
.id(1)
|
||||
.company_id(101)
|
||||
.buyer_name("John Doe")
|
||||
.buyer_email("john.doe@example.com")
|
||||
.currency_code("USD")
|
||||
.status(SaleStatus::Pending)
|
||||
.add_item(item1)
|
||||
.build()?;
|
||||
|
||||
// Insert the sale using model-specific methods
|
||||
db.insert_sale(sale.clone())?;
|
||||
println!(
|
||||
"Sale created: #{} for {} (${:.2})",
|
||||
sale.id, sale.buyer_name, sale.total_amount.amount
|
||||
);
|
||||
|
||||
println!("\n5. Updating a Sale");
|
||||
println!("-----------------");
|
||||
|
||||
// Retrieve the sale, update it, and save it back
|
||||
let mut retrieved_sale = db.get_sale(1)?;
|
||||
println!(
|
||||
"Retrieved sale: #{} with status {:?}",
|
||||
retrieved_sale.id, retrieved_sale.status
|
||||
);
|
||||
|
||||
// Update the status
|
||||
retrieved_sale.update_status(SaleStatus::Completed);
|
||||
db.insert_sale(retrieved_sale.clone())?;
|
||||
|
||||
println!("Updated sale status to {:?}", retrieved_sale.status);
|
||||
|
||||
println!("\n6. Deleting Objects");
|
||||
println!("------------------");
|
||||
|
||||
// Delete a product
|
||||
db.delete_product(2)?;
|
||||
println!("Deleted product #2");
|
||||
|
||||
// List remaining products
|
||||
let remaining_products = db.list_products()?;
|
||||
println!("Remaining products: {}", remaining_products.len());
|
||||
for product in remaining_products {
|
||||
println!(" - {}", product.name);
|
||||
}
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
7
herodb/src/cmd/mod.rs
Normal file
7
herodb/src/cmd/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//! Command examples for HeroDB
|
||||
//!
|
||||
//! This module contains various example commands and applications
|
||||
//! that demonstrate how to use HeroDB in different scenarios.
|
||||
|
||||
// Export the example modules
|
||||
pub mod dbexample_biz;
|
@ -1,5 +1,6 @@
|
||||
use bincode;
|
||||
use brotli::{CompressorReader, Decompressor};
|
||||
use rhai::CustomType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sled;
|
||||
use std::fmt::Debug;
|
||||
@ -38,15 +39,11 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||
let mut compressed = Vec::new();
|
||||
// Default Brotli parameters: quality 5, lgwin 22 (window size)
|
||||
const BROTLI_QUALITY: u32 = 5;
|
||||
const BROTLI_LGWIN: u32 = 22;
|
||||
const BROTLI_LGWIN: u32 = 22;
|
||||
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||
|
||||
let mut compressor = CompressorReader::new(
|
||||
&encoded[..],
|
||||
BUFFER_SIZE,
|
||||
BROTLI_QUALITY,
|
||||
BROTLI_LGWIN
|
||||
);
|
||||
let mut compressor =
|
||||
CompressorReader::new(&encoded[..], BUFFER_SIZE, BROTLI_QUALITY, BROTLI_LGWIN);
|
||||
compressor.read_to_end(&mut compressed)?;
|
||||
|
||||
Ok(compressed)
|
||||
@ -56,7 +53,7 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||
fn load_from_bytes(data: &[u8]) -> SledDBResult<Self> {
|
||||
let mut decompressed = Vec::new();
|
||||
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||
|
||||
|
||||
let mut decompressor = Decompressor::new(data, BUFFER_SIZE);
|
||||
decompressor.read_to_end(&mut decompressed)?;
|
||||
|
||||
@ -140,8 +137,8 @@ impl<T: SledModel> SledDB<T> {
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
/// Provides access to the underlying Sled Db instance for advanced operations.
|
||||
pub fn raw_db(&self) -> &sled::Db {
|
||||
/// Provides access to the underlying Sled Db instance for advanced operations.
|
||||
pub fn raw_db(&self) -> &sled::Db {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::db::base::*;
|
||||
use bincode;
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::fmt::Debug;
|
||||
use bincode;
|
||||
|
||||
/// Represents a single database operation in a transaction
|
||||
#[derive(Debug, Clone)]
|
||||
@ -24,7 +25,7 @@ pub trait AnyDbOperations: Send + Sync {
|
||||
fn delete(&self, id: &str) -> SledDBResult<()>;
|
||||
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
|
||||
fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()>;
|
||||
}
|
||||
|
||||
@ -33,17 +34,17 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
|
||||
fn delete(&self, id: &str) -> SledDBResult<()> {
|
||||
self.delete(id)
|
||||
}
|
||||
|
||||
|
||||
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||
let result = self.get(id)?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
|
||||
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||
let result = self.list()?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> {
|
||||
// Downcast to the specific type T
|
||||
match model.downcast_ref::<T>() {
|
||||
@ -51,7 +52,7 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()> {
|
||||
// Deserialize the bytes into model of type T
|
||||
let model: T = bincode::deserialize(serialized)?;
|
||||
@ -77,23 +78,25 @@ impl TransactionState {
|
||||
}
|
||||
|
||||
/// Main DB manager that automatically handles all root models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DB {
|
||||
db_path: PathBuf,
|
||||
|
||||
|
||||
// Type map for generic operations
|
||||
type_map: HashMap<TypeId, Box<dyn AnyDbOperations>>,
|
||||
|
||||
type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>>,
|
||||
|
||||
// Locks to ensure thread safety for key areas
|
||||
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
|
||||
|
||||
|
||||
// Transaction state
|
||||
transaction: RwLock<Option<TransactionState>>,
|
||||
transaction: Arc<RwLock<Option<TransactionState>>>,
|
||||
}
|
||||
|
||||
/// Builder for DB that allows registering models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DBBuilder {
|
||||
base_path: PathBuf,
|
||||
model_registrations: Vec<Box<dyn ModelRegistration>>,
|
||||
model_registrations: Vec<Arc<dyn ModelRegistration>>,
|
||||
}
|
||||
|
||||
/// Trait for model registration
|
||||
@ -129,33 +132,45 @@ impl DBBuilder {
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn with_path<P: Into<PathBuf>>(base_path: P) -> Self {
|
||||
Self {
|
||||
base_path: base_path.into(),
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a model type with the DB
|
||||
pub fn register_model<T: SledModel>(mut self) -> Self {
|
||||
self.model_registrations.push(Box::new(SledModelRegistration::<T>::new()));
|
||||
self.model_registrations
|
||||
.push(Arc::new(SledModelRegistration::<T>::new()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Build the DB with the registered models
|
||||
pub fn build(self) -> SledDBResult<DB> {
|
||||
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
|
||||
let base_path = self.base_path;
|
||||
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
std::fs::create_dir_all(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e))
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
// Register all models
|
||||
let mut type_map: HashMap<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
|
||||
|
||||
let mut type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>> = HashMap::new();
|
||||
|
||||
for registration in self.model_registrations {
|
||||
let (type_id, db) = registration.register(&base_path)?;
|
||||
type_map.insert(type_id, db);
|
||||
let (type_id, db) = registration.register(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e))
|
||||
})?;
|
||||
type_map.insert(type_id, db.into());
|
||||
}
|
||||
|
||||
|
||||
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||
let transaction = RwLock::new(None);
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(DB {
|
||||
db_path: base_path,
|
||||
type_map,
|
||||
@ -169,15 +184,15 @@ impl DB {
|
||||
/// Create a new empty DB instance without any models
|
||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
|
||||
let base_path = base_path.into();
|
||||
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
}
|
||||
|
||||
|
||||
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||
let transaction = RwLock::new(None);
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(Self {
|
||||
db_path: base_path,
|
||||
type_map: HashMap::new(),
|
||||
@ -185,25 +200,27 @@ impl DB {
|
||||
transaction,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Transaction-related methods
|
||||
|
||||
|
||||
/// Begin a new transaction
|
||||
pub fn begin_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
if tx.is_some() {
|
||||
return Err(SledDBError::GeneralError("Transaction already in progress".into()));
|
||||
return Err(SledDBError::GeneralError(
|
||||
"Transaction already in progress".into(),
|
||||
));
|
||||
}
|
||||
*tx = Some(TransactionState::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Check if a transaction is active
|
||||
pub fn has_active_transaction(&self) -> bool {
|
||||
let tx = self.transaction.read().unwrap();
|
||||
tx.is_some() && tx.as_ref().unwrap().active
|
||||
}
|
||||
|
||||
|
||||
/// Apply a set operation with the serialized data - bypass transaction check
|
||||
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> {
|
||||
// Get the database operations for this model type
|
||||
@ -211,39 +228,47 @@ impl DB {
|
||||
// Just pass the raw serialized data to a special raw insert method
|
||||
return db_ops.insert_any_raw(serialized);
|
||||
}
|
||||
|
||||
Err(SledDBError::GeneralError(format!("No DB registered for type ID {:?}", model_type)))
|
||||
|
||||
Err(SledDBError::GeneralError(format!(
|
||||
"No DB registered for type ID {:?}",
|
||||
model_type
|
||||
)))
|
||||
}
|
||||
|
||||
/// Commit the current transaction, applying all operations
|
||||
pub fn commit_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
if let Some(tx_state) = tx_guard.take() {
|
||||
if !tx_state.active {
|
||||
return Err(SledDBError::GeneralError("Transaction not active".into()));
|
||||
}
|
||||
|
||||
|
||||
// Execute all operations in the transaction
|
||||
for op in tx_state.operations {
|
||||
match op {
|
||||
DbOperation::Set { model_type, serialized } => {
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
} => {
|
||||
self.apply_set_operation(model_type, &serialized)?;
|
||||
},
|
||||
}
|
||||
DbOperation::Delete { model_type, id } => {
|
||||
let db_ops = self.type_map.get(&model_type)
|
||||
let db_ops = self
|
||||
.type_map
|
||||
.get(&model_type)
|
||||
.ok_or_else(|| SledDBError::TypeError)?;
|
||||
db_ops.delete(&id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SledDBError::GeneralError("No active transaction".into()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Rollback the current transaction, discarding all operations
|
||||
pub fn rollback_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
@ -253,79 +278,85 @@ impl DB {
|
||||
*tx = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get the path to the database
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.db_path
|
||||
}
|
||||
|
||||
|
||||
// Generic methods that work with any supported model type
|
||||
|
||||
|
||||
/// Insert a model instance into its appropriate database based on type
|
||||
pub fn set<T: SledModel>(&self, model: &T) -> SledDBResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
// Serialize the model for later use
|
||||
let serialized = bincode::serialize(model)?;
|
||||
|
||||
|
||||
// Record a Set operation in the transaction
|
||||
tx_state.operations.push(DbOperation::Set {
|
||||
model_type: TypeId::of::<T>(),
|
||||
serialized,
|
||||
});
|
||||
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => db_ops.insert_any(model),
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check the transaction state for the given type and id
|
||||
fn check_transaction<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, SledDBError>> {
|
||||
// Try to acquire a read lock on the transaction
|
||||
let tx_guard = self.transaction.read().unwrap();
|
||||
|
||||
|
||||
if let Some(tx_state) = tx_guard.as_ref() {
|
||||
if !tx_state.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let type_id = TypeId::of::<T>();
|
||||
let id_str = id.to_string();
|
||||
|
||||
|
||||
// Process operations in reverse order (last operation wins)
|
||||
for op in tx_state.operations.iter().rev() {
|
||||
match op {
|
||||
// First check if this ID has been deleted in the transaction
|
||||
DbOperation::Delete { model_type, id: op_id } => {
|
||||
DbOperation::Delete {
|
||||
model_type,
|
||||
id: op_id,
|
||||
} => {
|
||||
if *model_type == type_id && op_id == id {
|
||||
// Return NotFound error for deleted records
|
||||
return Some(Err(SledDBError::NotFound(id.to_string())));
|
||||
}
|
||||
},
|
||||
}
|
||||
// Then check if it has been set in the transaction
|
||||
DbOperation::Set { model_type, serialized } => {
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
} => {
|
||||
if *model_type == type_id {
|
||||
// Try to deserialize and check the ID
|
||||
match bincode::deserialize::<T>(serialized) {
|
||||
Ok(model) => {
|
||||
if model.get_id() == id_str {
|
||||
return Some(Ok(Some(model)));
|
||||
if model.get_id() == id_str {
|
||||
return Some(Ok(Some(model)));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => continue, // Skip if deserialization fails
|
||||
}
|
||||
}
|
||||
@ -333,7 +364,7 @@ impl DB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Not found in transaction (continue to database)
|
||||
None
|
||||
}
|
||||
@ -348,7 +379,7 @@ impl DB {
|
||||
Ok(None) => {} // Should never happen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no pending value, look up from the database
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
@ -358,16 +389,16 @@ impl DB {
|
||||
Ok(t) => Ok(*t),
|
||||
Err(_) => Err(SledDBError::TypeError),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete a model instance by its ID and type
|
||||
pub fn delete<T: SledModel>(&self, id: &str) -> SledDBResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
@ -376,22 +407,22 @@ impl DB {
|
||||
model_type: TypeId::of::<T>(),
|
||||
id: id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => db_ops.delete(id),
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// List all model instances of a specific type
|
||||
pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
|
||||
// Look up the correct DB operations for type T in our type map
|
||||
@ -403,24 +434,27 @@ impl DB {
|
||||
Ok(vec_t) => Ok(*vec_t),
|
||||
Err(_) => Err(SledDBError::TypeError),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Register a model type with this DB instance
|
||||
pub fn register<T: SledModel>(&mut self) -> SledDBResult<()> {
|
||||
let db_path = self.db_path.join(T::db_prefix());
|
||||
let db: SledDB<T> = SledDB::open(db_path)?;
|
||||
self.type_map.insert(TypeId::of::<T>(), Box::new(db));
|
||||
self.type_map.insert(TypeId::of::<T>(), Arc::new(db));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// Get a typed handle to a registered model DB
|
||||
pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db) => Ok(&**db),
|
||||
None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::<T>()))),
|
||||
None => Err(SledDBError::GeneralError(format!(
|
||||
"No DB registered for type {}",
|
||||
std::any::type_name::<T>()
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,27 @@ macro_rules! impl_model_methods {
|
||||
impl DB {
|
||||
paste::paste! {
|
||||
/// Insert a model instance into the database
|
||||
pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
|
||||
self.set(item)
|
||||
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
Ok(self.set(&item).map_err(|e| {
|
||||
rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e))
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Get a model instance by its ID
|
||||
pub fn [<get_ $singular>](&self, id: u32) -> SledDBResult<$model> {
|
||||
pub fn [<get_ $singular>](&mut self, id: i64) -> SledDBResult<$model> {
|
||||
self.get::<$model>(&id.to_string())
|
||||
}
|
||||
|
||||
/// Delete a model instance by its ID
|
||||
pub fn [<delete_ $singular>](&self, id: u32) -> SledDBResult<()> {
|
||||
pub fn [<delete_ $singular>](&mut self, id: i64) -> SledDBResult<()> {
|
||||
self.delete::<$model>(&id.to_string())
|
||||
}
|
||||
|
||||
/// List all model instances
|
||||
pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
|
||||
pub fn [<list_ $plural>](&mut self) -> SledDBResult<Vec<$model>> {
|
||||
self.list::<$model>()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,9 @@
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
// Temporarily commented out due to compilation errors
|
||||
// pub mod rhaiengine;
|
||||
pub mod cmd;
|
||||
|
||||
// Re-exports
|
||||
pub use error::Error;
|
||||
|
@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization
|
||||
## Model Relationships
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │
|
||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
▲ │
|
||||
│ │
|
||||
┌─────┴──────────┐ │
|
||||
│ProductComponent│ │
|
||||
└────────────────┘ │
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Sale │
|
||||
└─────────────┘
|
||||
│ Customer │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
▲ │
|
||||
│ │
|
||||
┌─────┴──────────┐ │
|
||||
│ProductComponent│ │
|
||||
└────────────────┘ │
|
||||
│
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ InvoiceItem │◄────┤ Invoice │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Business Logic Relationships
|
||||
|
||||
- **Customer**: The entity purchasing products or services
|
||||
- **Product/Service**: Defines what is being sold, including its base price
|
||||
- Can be marked as a template (`is_template=true`) to create copies for actual sales
|
||||
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
|
||||
- Can be linked to a Service when the sale creates an ongoing service
|
||||
- **Service**: Represents an ongoing service provided to a customer
|
||||
- Created from a Product template when the product type is Service
|
||||
- **Invoice**: Represents the billing document for a sale, with payment tracking
|
||||
- Created from a Sale object to handle billing and payment tracking
|
||||
|
||||
## Root Objects
|
||||
|
||||
- root objects are the one who are stored in the DB
|
||||
- Root Objects are
|
||||
- currency
|
||||
- product
|
||||
- Root objects are the ones stored directly in the DB
|
||||
- Root Objects are:
|
||||
- Customer
|
||||
- Currency
|
||||
- Product
|
||||
- Sale
|
||||
- Service
|
||||
- Invoice
|
||||
|
||||
## Models
|
||||
|
||||
@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code.
|
||||
**Builder:**
|
||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
||||
|
||||
### Customer (Root Object)
|
||||
|
||||
Represents a customer who can purchase products or services.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Customer name
|
||||
- `description`: String - Customer description
|
||||
- `pubkey`: String - Customer's public key
|
||||
- `contact_ids`: Vec<u32> - List of contact IDs
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Methods:**
|
||||
- `add_contact()` - Adds a contact ID to the customer
|
||||
- `remove_contact()` - Removes a contact ID from the customer
|
||||
|
||||
### Product
|
||||
|
||||
#### ProductType Enum
|
||||
@ -70,11 +113,11 @@ Represents a component part of a product.
|
||||
**Builder:**
|
||||
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
|
||||
|
||||
#### Product (Root Object)
|
||||
#### Product (Root Object)
|
||||
Represents a product or service offered.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `id`: i64 - Unique identifier
|
||||
- `name`: String - Product name
|
||||
- `description`: String - Product description
|
||||
- `price`: Currency - Product price
|
||||
@ -83,10 +126,11 @@ Represents a product or service offered.
|
||||
- `status`: ProductStatus - Available or Unavailable
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `max_amount`: u16 - Maximum quantity available
|
||||
- `max_amount`: i64 - Maximum quantity available
|
||||
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||
- `active_till`: DateTime<Utc> - When product/service expires
|
||||
- `components`: Vec<ProductComponent> - List of product components
|
||||
- `is_template`: bool - Whether this is a template product (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_component()` - Adds a component to this product
|
||||
@ -104,7 +148,62 @@ Represents a product or service offered.
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `db_prefix()` - Returns "product" as the database prefix
|
||||
|
||||
### Sale
|
||||
### Service (Root Object)
|
||||
|
||||
#### BillingFrequency Enum
|
||||
Defines how often a service is billed:
|
||||
- `Hourly` - Billed by the hour
|
||||
- `Daily` - Billed daily
|
||||
- `Weekly` - Billed weekly
|
||||
- `Monthly` - Billed monthly
|
||||
- `Yearly` - Billed yearly
|
||||
|
||||
#### ServiceStatus Enum
|
||||
Tracks the status of a service:
|
||||
- `Active` - Service is currently active
|
||||
- `Paused` - Service is temporarily paused
|
||||
- `Cancelled` - Service has been cancelled
|
||||
- `Completed` - Service has been completed
|
||||
|
||||
#### ServiceItem
|
||||
Represents an item within a service.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `service_id`: u32 - Parent service ID
|
||||
- `product_id`: u32 - ID of the product this service is based on
|
||||
- `name`: String - Service name
|
||||
- `description`: String - Detailed description of the service item
|
||||
- `comments`: String - Additional notes or comments about the service item
|
||||
- `quantity`: i32 - Number of units
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price before tax
|
||||
- `tax_rate`: f64 - Tax rate as a percentage
|
||||
- `tax_amount`: Currency - Calculated tax amount
|
||||
- `is_taxable`: bool - Whether this item is taxable
|
||||
- `active_till`: DateTime<Utc> - When service expires
|
||||
|
||||
#### Service
|
||||
Represents an ongoing service provided to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer receiving the service
|
||||
- `total_amount`: Currency - Total service amount including tax
|
||||
- `status`: ServiceStatus - Current service status
|
||||
- `billing_frequency`: BillingFrequency - How often the service is billed
|
||||
- `service_date`: DateTime<Utc> - When service started
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<ServiceItem> - List of items in the service
|
||||
- `is_template`: bool - Whether this is a template service (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the service and updates total
|
||||
- `calculate_total()` - Recalculates the total amount
|
||||
- `update_status()` - Updates the status of the service
|
||||
|
||||
### Sale
|
||||
|
||||
#### SaleStatus Enum
|
||||
Tracks the status of a sale:
|
||||
@ -120,11 +219,18 @@ Represents an item within a sale.
|
||||
- `sale_id`: u32 - Parent sale ID
|
||||
- `product_id`: u32 - ID of the product sold
|
||||
- `name`: String - Product name at time of sale
|
||||
- `description`: String - Detailed description of the item
|
||||
- `comments`: String - Additional notes or comments about the item
|
||||
- `quantity`: i32 - Number of items purchased
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price for this item (calculated)
|
||||
- `subtotal`: Currency - Total price for this item before tax (calculated)
|
||||
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
- `tax_amount`: Currency - Calculated tax amount for this item
|
||||
- `active_till`: DateTime<Utc> - When item/service expires
|
||||
|
||||
**Methods:**
|
||||
- `total_with_tax()` - Returns the total amount including tax
|
||||
|
||||
**Builder:**
|
||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||
|
||||
@ -134,18 +240,24 @@ Represents a complete sale transaction.
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `company_id`: u32 - ID of the company making the sale
|
||||
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
|
||||
- `buyer_name`: String - Name of the buyer
|
||||
- `buyer_email`: String - Email of the buyer
|
||||
- `total_amount`: Currency - Total sale amount
|
||||
- `subtotal_amount`: Currency - Total sale amount before tax
|
||||
- `tax_amount`: Currency - Total tax amount for the sale
|
||||
- `total_amount`: Currency - Total sale amount including tax
|
||||
- `status`: SaleStatus - Current sale status
|
||||
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
|
||||
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<SaleItem> - List of items in the sale
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the sale and updates total
|
||||
- `add_item()` - Adds an item to the sale and updates totals
|
||||
- `update_status()` - Updates the status of the sale
|
||||
- `recalculate_totals()` - Recalculates all totals based on items
|
||||
- `create_service()` - Creates a service from this sale (to be added)
|
||||
|
||||
**Builder:**
|
||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||
@ -223,6 +335,7 @@ let item = SaleItemBuilder::new()
|
||||
.name("Premium Service")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0) // 20% tax rate
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new()
|
||||
|
||||
// Update the sale status
|
||||
sale.update_status(SaleStatus::Completed);
|
||||
|
||||
// The sale now contains:
|
||||
// - subtotal_amount: The sum of all items before tax
|
||||
// - tax_amount: The sum of all tax amounts
|
||||
// - total_amount: The total including tax
|
||||
```
|
||||
|
||||
### Relationship Between Sale and Invoice
|
||||
|
||||
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
|
||||
|
||||
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
|
||||
|
||||
```rust
|
||||
// Create an invoice item linked to a sale
|
||||
let invoice_item = InvoiceItemBuilder::new()
|
||||
.id(1)
|
||||
.invoice_id(1)
|
||||
.description("Premium Service")
|
||||
.amount(sale.total_amount.clone()) // Use the total amount from the sale
|
||||
.sale_id(sale.id) // Link to the sale
|
||||
.build()
|
||||
.expect("Failed to build invoice item");
|
||||
```
|
||||
|
||||
## Database Operations
|
||||
@ -266,4 +402,125 @@ These methods are available for all root objects:
|
||||
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
||||
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
|
||||
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
|
||||
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
|
||||
|
||||
### Invoice (Root Object)
|
||||
|
||||
#### InvoiceStatus Enum
|
||||
Tracks the status of an invoice:
|
||||
- `Draft` - Invoice is in draft state
|
||||
- `Sent` - Invoice has been sent to the customer
|
||||
- `Paid` - Invoice has been paid
|
||||
- `Overdue` - Invoice is past due date
|
||||
- `Cancelled` - Invoice has been cancelled
|
||||
|
||||
#### PaymentStatus Enum
|
||||
Tracks the payment status of an invoice:
|
||||
- `Unpaid` - Invoice has not been paid
|
||||
- `PartiallyPaid` - Invoice has been partially paid
|
||||
- `Paid` - Invoice has been fully paid
|
||||
|
||||
#### Payment
|
||||
Represents a payment made against an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: Currency - Payment amount
|
||||
- `date`: DateTime<Utc> - Payment date
|
||||
- `method`: String - Payment method
|
||||
- `comment`: String - Payment comment
|
||||
|
||||
#### InvoiceItem
|
||||
Represents an item in an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `invoice_id`: u32 - Parent invoice ID
|
||||
- `description`: String - Item description
|
||||
- `amount`: Currency - Item amount
|
||||
- `service_id`: Option<u32> - ID of the service this item is for
|
||||
- `sale_id`: Option<u32> - ID of the sale this item is for
|
||||
|
||||
**Methods:**
|
||||
- `link_to_service()` - Links the invoice item to a service
|
||||
- `link_to_sale()` - Links the invoice item to a sale
|
||||
|
||||
#### Invoice
|
||||
Represents an invoice sent to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer being invoiced
|
||||
- `total_amount`: Currency - Total invoice amount
|
||||
- `balance_due`: Currency - Amount still due
|
||||
- `status`: InvoiceStatus - Current invoice status
|
||||
- `payment_status`: PaymentStatus - Current payment status
|
||||
- `issue_date`: DateTime<Utc> - When invoice was issued
|
||||
- `due_date`: DateTime<Utc> - When payment is due
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<InvoiceItem> - List of items in the invoice
|
||||
- `payments`: Vec<Payment> - List of payments made
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the invoice
|
||||
- `calculate_total()` - Calculates the total amount
|
||||
- `add_payment()` - Adds a payment to the invoice
|
||||
- `calculate_balance()` - Calculates the balance due
|
||||
- `update_payment_status()` - Updates the payment status
|
||||
- `update_status()` - Updates the status of the invoice
|
||||
- `is_overdue()` - Checks if the invoice is overdue
|
||||
- `check_if_overdue()` - Marks the invoice as overdue if past due date
|
||||
|
||||
### Relationships Between Models
|
||||
|
||||
#### Product/Service Templates and Instances
|
||||
|
||||
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
|
||||
|
||||
#### Sale to Service Relationship
|
||||
|
||||
When a product of type `Service` is sold, a Service instance can be created from the Sale:
|
||||
|
||||
```rust
|
||||
// Create a service from a sale
|
||||
let service = sale.create_service(
|
||||
service_id,
|
||||
ServiceStatus::Active,
|
||||
BillingFrequency::Monthly
|
||||
);
|
||||
```
|
||||
|
||||
#### Sale to Invoice Relationship
|
||||
|
||||
An Invoice is created from a Sale to handle billing and payment tracking:
|
||||
|
||||
```rust
|
||||
// Create an invoice from a sale
|
||||
let invoice = Invoice::from_sale(
|
||||
invoice_id,
|
||||
sale,
|
||||
due_date
|
||||
);
|
||||
```
|
||||
|
||||
#### Customer-Centric View
|
||||
|
||||
The models allow tracking all customer interactions:
|
||||
|
||||
- What products/services they've purchased (via Sale records)
|
||||
- What ongoing services they have (via Service records)
|
||||
- What they've been invoiced for (via Invoice records)
|
||||
- What they've paid (via Payment records in Invoices)
|
||||
|
||||
```rust
|
||||
// Get all sales for a customer
|
||||
let customer_sales = db.list_sales_by_customer(customer_id);
|
||||
|
||||
// Get all services for a customer
|
||||
let customer_services = db.list_services_by_customer(customer_id);
|
||||
|
||||
// Get all invoices for a customer
|
||||
let customer_invoices = db.list_invoices_by_customer(customer_id);
|
||||
```
|
||||
|
@ -1,371 +0,0 @@
|
||||
# Business Models Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the plan for implementing new business models in the codebase:
|
||||
|
||||
1. **Service**: For tracking recurring payments (similar to Sale)
|
||||
2. **Customer**: For storing customer information
|
||||
3. **Contract**: For linking services or sales to customers
|
||||
4. **Invoice**: For invoicing customers
|
||||
|
||||
## Model Diagrams
|
||||
|
||||
### Core Models and Relationships
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Service {
|
||||
+id: u32
|
||||
+customer_id: u32
|
||||
+total_amount: Currency
|
||||
+status: ServiceStatus
|
||||
+billing_frequency: BillingFrequency
|
||||
+service_date: DateTime~Utc~
|
||||
+created_at: DateTime~Utc~
|
||||
+updated_at: DateTime~Utc~
|
||||
+items: Vec~ServiceItem~
|
||||
+calculate_total()
|
||||
}
|
||||
|
||||
class ServiceItem {
|
||||
+id: u32
|
||||
+service_id: u32
|
||||
+name: String
|
||||
+quantity: i32
|
||||
+unit_price: Currency
|
||||
+subtotal: Currency
|
||||
+tax_rate: f64
|
||||
+tax_amount: Currency
|
||||
+is_taxable: bool
|
||||
+active_till: DateTime~Utc~
|
||||
}
|
||||
|
||||
class Customer {
|
||||
+id: u32
|
||||
+name: String
|
||||
+description: String
|
||||
+pubkey: String
|
||||
+contact_ids: Vec~u32~
|
||||
+created_at: DateTime~Utc~
|
||||
+updated_at: DateTime~Utc~
|
||||
}
|
||||
|
||||
class Contract {
|
||||
+id: u32
|
||||
+customer_id: u32
|
||||
+service_id: Option~u32~
|
||||
+sale_id: Option~u32~
|
||||
+terms: String
|
||||
+start_date: DateTime~Utc~
|
||||
+end_date: DateTime~Utc~
|
||||
+auto_renewal: bool
|
||||
+renewal_terms: String
|
||||
+status: ContractStatus
|
||||
+created_at: DateTime~Utc~
|
||||
+updated_at: DateTime~Utc~
|
||||
}
|
||||
|
||||
class Invoice {
|
||||
+id: u32
|
||||
+customer_id: u32
|
||||
+total_amount: Currency
|
||||
+balance_due: Currency
|
||||
+status: InvoiceStatus
|
||||
+payment_status: PaymentStatus
|
||||
+issue_date: DateTime~Utc~
|
||||
+due_date: DateTime~Utc~
|
||||
+created_at: DateTime~Utc~
|
||||
+updated_at: DateTime~Utc~
|
||||
+items: Vec~InvoiceItem~
|
||||
+payments: Vec~Payment~
|
||||
}
|
||||
|
||||
class InvoiceItem {
|
||||
+id: u32
|
||||
+invoice_id: u32
|
||||
+description: String
|
||||
+amount: Currency
|
||||
+service_id: Option~u32~
|
||||
+sale_id: Option~u32~
|
||||
}
|
||||
|
||||
class Payment {
|
||||
+amount: Currency
|
||||
+date: DateTime~Utc~
|
||||
+method: String
|
||||
}
|
||||
|
||||
Service "1" -- "many" ServiceItem : contains
|
||||
Customer "1" -- "many" Service : has
|
||||
Customer "1" -- "many" Contract : has
|
||||
Contract "1" -- "0..1" Service : references
|
||||
Contract "1" -- "0..1" Sale : references
|
||||
Invoice "1" -- "many" InvoiceItem : contains
|
||||
Invoice "1" -- "many" Payment : contains
|
||||
Customer "1" -- "many" Invoice : has
|
||||
InvoiceItem "1" -- "0..1" Service : references
|
||||
InvoiceItem "1" -- "0..1" Sale : references
|
||||
```
|
||||
|
||||
### Enums and Supporting Types
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class BillingFrequency {
|
||||
<<enumeration>>
|
||||
Hourly
|
||||
Daily
|
||||
Weekly
|
||||
Monthly
|
||||
Yearly
|
||||
}
|
||||
|
||||
class ServiceStatus {
|
||||
<<enumeration>>
|
||||
Active
|
||||
Paused
|
||||
Cancelled
|
||||
Completed
|
||||
}
|
||||
|
||||
class ContractStatus {
|
||||
<<enumeration>>
|
||||
Active
|
||||
Expired
|
||||
Terminated
|
||||
}
|
||||
|
||||
class InvoiceStatus {
|
||||
<<enumeration>>
|
||||
Draft
|
||||
Sent
|
||||
Paid
|
||||
Overdue
|
||||
Cancelled
|
||||
}
|
||||
|
||||
class PaymentStatus {
|
||||
<<enumeration>>
|
||||
Unpaid
|
||||
PartiallyPaid
|
||||
Paid
|
||||
}
|
||||
|
||||
Service -- ServiceStatus : has
|
||||
Service -- BillingFrequency : has
|
||||
Contract -- ContractStatus : has
|
||||
Invoice -- InvoiceStatus : has
|
||||
Invoice -- PaymentStatus : has
|
||||
```
|
||||
|
||||
## Detailed Implementation Plan
|
||||
|
||||
### 1. Service and ServiceItem (service.rs)
|
||||
|
||||
The Service model will be similar to Sale but designed for recurring payments:
|
||||
|
||||
- **Service**: Main struct for tracking recurring services
|
||||
- Fields:
|
||||
- id: u32
|
||||
- customer_id: u32
|
||||
- total_amount: Currency
|
||||
- status: ServiceStatus
|
||||
- billing_frequency: BillingFrequency
|
||||
- service_date: DateTime<Utc>
|
||||
- created_at: DateTime<Utc>
|
||||
- updated_at: DateTime<Utc>
|
||||
- items: Vec<ServiceItem>
|
||||
- Methods:
|
||||
- calculate_total(): Updates the total_amount based on all items
|
||||
- add_item(item: ServiceItem): Adds an item and updates the total
|
||||
- update_status(status: ServiceStatus): Updates the status and timestamp
|
||||
|
||||
- **ServiceItem**: Items within a service (similar to SaleItem)
|
||||
- Fields:
|
||||
- id: u32
|
||||
- service_id: u32
|
||||
- name: String
|
||||
- quantity: i32
|
||||
- unit_price: Currency
|
||||
- subtotal: Currency
|
||||
- tax_rate: f64
|
||||
- tax_amount: Currency
|
||||
- is_taxable: bool
|
||||
- active_till: DateTime<Utc>
|
||||
- Methods:
|
||||
- calculate_subtotal(): Calculates subtotal based on quantity and unit_price
|
||||
- calculate_tax(): Calculates tax amount based on subtotal and tax_rate
|
||||
|
||||
- **BillingFrequency**: Enum for different billing periods
|
||||
- Variants: Hourly, Daily, Weekly, Monthly, Yearly
|
||||
|
||||
- **ServiceStatus**: Enum for service status
|
||||
- Variants: Active, Paused, Cancelled, Completed
|
||||
|
||||
### 2. Customer (customer.rs)
|
||||
|
||||
The Customer model will store customer information:
|
||||
|
||||
- **Customer**: Main struct for customer data
|
||||
- Fields:
|
||||
- id: u32
|
||||
- name: String
|
||||
- description: String
|
||||
- pubkey: String
|
||||
- contact_ids: Vec<u32>
|
||||
- created_at: DateTime<Utc>
|
||||
- updated_at: DateTime<Utc>
|
||||
- Methods:
|
||||
- add_contact(contact_id: u32): Adds a contact ID to the list
|
||||
- remove_contact(contact_id: u32): Removes a contact ID from the list
|
||||
|
||||
### 3. Contract (contract.rs)
|
||||
|
||||
The Contract model will link services or sales to customers:
|
||||
|
||||
- **Contract**: Main struct for contract data
|
||||
- Fields:
|
||||
- id: u32
|
||||
- customer_id: u32
|
||||
- service_id: Option<u32>
|
||||
- sale_id: Option<u32>
|
||||
- terms: String
|
||||
- start_date: DateTime<Utc>
|
||||
- end_date: DateTime<Utc>
|
||||
- auto_renewal: bool
|
||||
- renewal_terms: String
|
||||
- status: ContractStatus
|
||||
- created_at: DateTime<Utc>
|
||||
- updated_at: DateTime<Utc>
|
||||
- Methods:
|
||||
- is_active(): bool - Checks if the contract is currently active
|
||||
- is_expired(): bool - Checks if the contract has expired
|
||||
- renew(): Updates the contract dates based on renewal terms
|
||||
|
||||
- **ContractStatus**: Enum for contract status
|
||||
- Variants: Active, Expired, Terminated
|
||||
|
||||
### 4. Invoice (invoice.rs)
|
||||
|
||||
The Invoice model will handle billing:
|
||||
|
||||
- **Invoice**: Main struct for invoice data
|
||||
- Fields:
|
||||
- id: u32
|
||||
- customer_id: u32
|
||||
- total_amount: Currency
|
||||
- balance_due: Currency
|
||||
- status: InvoiceStatus
|
||||
- payment_status: PaymentStatus
|
||||
- issue_date: DateTime<Utc>
|
||||
- due_date: DateTime<Utc>
|
||||
- created_at: DateTime<Utc>
|
||||
- updated_at: DateTime<Utc>
|
||||
- items: Vec<InvoiceItem>
|
||||
- payments: Vec<Payment>
|
||||
- Methods:
|
||||
- calculate_total(): Updates the total_amount based on all items
|
||||
- add_item(item: InvoiceItem): Adds an item and updates the total
|
||||
- add_payment(payment: Payment): Adds a payment and updates balance_due and payment_status
|
||||
- update_status(status: InvoiceStatus): Updates the status and timestamp
|
||||
- calculate_balance(): Updates the balance_due based on total_amount and payments
|
||||
|
||||
- **InvoiceItem**: Items within an invoice
|
||||
- Fields:
|
||||
- id: u32
|
||||
- invoice_id: u32
|
||||
- description: String
|
||||
- amount: Currency
|
||||
- service_id: Option<u32>
|
||||
- sale_id: Option<u32>
|
||||
|
||||
- **Payment**: Struct for tracking payments
|
||||
- Fields:
|
||||
- amount: Currency
|
||||
- date: DateTime<Utc>
|
||||
- method: String
|
||||
|
||||
- **InvoiceStatus**: Enum for invoice status
|
||||
- Variants: Draft, Sent, Paid, Overdue, Cancelled
|
||||
|
||||
- **PaymentStatus**: Enum for payment status
|
||||
- Variants: Unpaid, PartiallyPaid, Paid
|
||||
|
||||
### 5. Updates to mod.rs
|
||||
|
||||
We'll need to update the mod.rs file to include the new modules and re-export the types:
|
||||
|
||||
```rust
|
||||
pub mod currency;
|
||||
pub mod product;
|
||||
pub mod sale;
|
||||
pub mod exchange_rate;
|
||||
pub mod service;
|
||||
pub mod customer;
|
||||
pub mod contract;
|
||||
pub mod invoice;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||
pub use currency::Currency;
|
||||
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
|
||||
pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency};
|
||||
pub use customer::Customer;
|
||||
pub use contract::{Contract, ContractStatus};
|
||||
pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment};
|
||||
|
||||
// Re-export builder types
|
||||
pub use product::{ProductBuilder, ProductComponentBuilder};
|
||||
pub use sale::{SaleBuilder, SaleItemBuilder};
|
||||
pub use currency::CurrencyBuilder;
|
||||
pub use exchange_rate::ExchangeRateBuilder;
|
||||
pub use service::{ServiceBuilder, ServiceItemBuilder};
|
||||
pub use customer::CustomerBuilder;
|
||||
pub use contract::ContractBuilder;
|
||||
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};
|
||||
```
|
||||
|
||||
### 6. Updates to model_methods.rs
|
||||
|
||||
We'll need to update the model_methods.rs file to implement the model methods for the new models:
|
||||
|
||||
```rust
|
||||
use crate::db::db::DB;
|
||||
use crate::db::base::{SledDBResult, SledModel};
|
||||
use crate::impl_model_methods;
|
||||
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
|
||||
|
||||
// Implement model-specific methods for Product
|
||||
impl_model_methods!(Product, product, products);
|
||||
|
||||
// Implement model-specific methods for Sale
|
||||
impl_model_methods!(Sale, sale, sales);
|
||||
|
||||
// Implement model-specific methods for Currency
|
||||
impl_model_methods!(Currency, currency, currencies);
|
||||
|
||||
// Implement model-specific methods for ExchangeRate
|
||||
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
|
||||
|
||||
// Implement model-specific methods for Service
|
||||
impl_model_methods!(Service, service, services);
|
||||
|
||||
// Implement model-specific methods for Customer
|
||||
impl_model_methods!(Customer, customer, customers);
|
||||
|
||||
// Implement model-specific methods for Contract
|
||||
impl_model_methods!(Contract, contract, contracts);
|
||||
|
||||
// Implement model-specific methods for Invoice
|
||||
impl_model_methods!(Invoice, invoice, invoices);
|
||||
```
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
1. Create the new model files (service.rs, customer.rs, contract.rs, invoice.rs)
|
||||
2. Implement the structs, enums, and methods for each model
|
||||
3. Update mod.rs to include the new modules and re-export the types
|
||||
4. Update model_methods.rs to implement the model methods for the new models
|
||||
5. Test the new models with example code
|
@ -1,10 +1,10 @@
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||
|
||||
/// Currency represents a monetary value with amount and currency code
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Currency {
|
||||
pub amount: f64,
|
||||
pub currency_code: String,
|
||||
@ -19,28 +19,13 @@ impl Currency {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the currency to USD
|
||||
pub fn to_usd(&self) -> Option<Currency> {
|
||||
if self.currency_code == "USD" {
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, "USD")
|
||||
.map(|amount| Currency::new(amount, "USD".to_string()))
|
||||
}
|
||||
|
||||
/// Convert the currency to another currency
|
||||
pub fn to_currency(&self, target_currency: &str) -> Option<Currency> {
|
||||
if self.currency_code == target_currency {
|
||||
return Some(self.clone());
|
||||
}
|
||||
|
||||
EXCHANGE_RATE_SERVICE.convert(self.amount, &self.currency_code, target_currency)
|
||||
.map(|amount| Currency::new(amount, target_currency.to_string()))
|
||||
pub fn amount(&mut self) -> f64 {
|
||||
self.amount
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Currency
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct CurrencyBuilder {
|
||||
amount: Option<f64>,
|
||||
currency_code: Option<String>,
|
||||
@ -68,7 +53,7 @@ impl CurrencyBuilder {
|
||||
}
|
||||
|
||||
/// Build the Currency object
|
||||
pub fn build(self) -> Result<Currency, &'static str> {
|
||||
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
|
||||
Ok(Currency {
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
||||
|
@ -27,15 +27,17 @@ pub struct Payment {
|
||||
pub amount: Currency,
|
||||
pub date: DateTime<Utc>,
|
||||
pub method: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl Payment {
|
||||
/// Create a new payment
|
||||
pub fn new(amount: Currency, method: String) -> Self {
|
||||
pub fn new(amount: Currency, method: String, comment: String) -> Self {
|
||||
Self {
|
||||
amount,
|
||||
date: Utc::now(),
|
||||
method,
|
||||
comment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||
|
||||
/// ProductType represents the type of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -13,10 +13,6 @@ pub enum ProductType {
|
||||
/// ProductStatus represents the status of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProductStatus {
|
||||
Active,
|
||||
Error,
|
||||
EndOfLife,
|
||||
Paused,
|
||||
Available,
|
||||
Unavailable,
|
||||
}
|
||||
@ -24,19 +20,17 @@ pub enum ProductStatus {
|
||||
/// ProductComponent represents a component of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProductComponent {
|
||||
pub id: u32,
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub quantity: i32,
|
||||
pub quantity: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub energy_usage: f64, // Energy usage in watts
|
||||
pub cost: Currency, // Cost of the component
|
||||
}
|
||||
|
||||
impl ProductComponent {
|
||||
/// Create a new product component with default timestamps
|
||||
pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self {
|
||||
pub fn new(id: i64, name: String, description: String, quantity: i64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
@ -45,32 +39,19 @@ impl ProductComponent {
|
||||
quantity,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
energy_usage: 0.0,
|
||||
cost: Currency::new(0.0, "USD".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total energy usage for this component (energy_usage * quantity)
|
||||
pub fn total_energy_usage(&self) -> f64 {
|
||||
self.energy_usage * self.quantity as f64
|
||||
}
|
||||
|
||||
/// Get the total cost for this component (cost * quantity)
|
||||
pub fn total_cost(&self) -> Currency {
|
||||
Currency::new(self.cost.amount * self.quantity as f64, self.cost.currency_code.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for ProductComponent
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductComponentBuilder {
|
||||
id: Option<u32>,
|
||||
id: Option<i64>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
quantity: Option<i64>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
energy_usage: Option<f64>,
|
||||
cost: Option<Currency>,
|
||||
}
|
||||
|
||||
impl ProductComponentBuilder {
|
||||
@ -83,13 +64,11 @@ impl ProductComponentBuilder {
|
||||
quantity: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
energy_usage: None,
|
||||
cost: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
pub fn id(mut self, id: i64) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
@ -107,7 +86,7 @@ impl ProductComponentBuilder {
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
pub fn quantity(mut self, quantity: i64) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
@ -124,20 +103,8 @@ impl ProductComponentBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the energy usage in watts
|
||||
pub fn energy_usage(mut self, energy_usage: f64) -> Self {
|
||||
self.energy_usage = Some(energy_usage);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cost
|
||||
pub fn cost(mut self, cost: Currency) -> Self {
|
||||
self.cost = Some(cost);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ProductComponent object
|
||||
pub fn build(self) -> Result<ProductComponent, &'static str> {
|
||||
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
let now = Utc::now();
|
||||
Ok(ProductComponent {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
@ -146,16 +113,19 @@ impl ProductComponentBuilder {
|
||||
quantity: self.quantity.ok_or("quantity is required")?,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
energy_usage: self.energy_usage.unwrap_or(0.0),
|
||||
cost: self.cost.unwrap_or_else(|| Currency::new(0.0, "USD".to_string())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// Product represents a product or service offered in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
=======
|
||||
/// Product represents a product or service offered by the Freezone
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
>>>>>>> builders_in_script
|
||||
pub struct Product {
|
||||
pub id: u32,
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: Currency,
|
||||
@ -164,7 +134,7 @@ pub struct Product {
|
||||
pub status: ProductStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub max_amount: u16, // means allows us to define how many max of this there are
|
||||
pub max_amount: i64, // means allows us to define how many max of this there are
|
||||
pub purchase_till: DateTime<Utc>,
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
pub components: Vec<ProductComponent>,
|
||||
@ -175,14 +145,14 @@ pub struct Product {
|
||||
impl Product {
|
||||
/// Create a new product with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
id: i64,
|
||||
name: String,
|
||||
description: String,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: u16,
|
||||
max_amount: i64,
|
||||
validity_days: i64, // How many days the product is valid after purchase
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
@ -203,86 +173,40 @@ impl Product {
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add a component to this product
|
||||
pub fn add_component(&mut self, component: ProductComponent) {
|
||||
self.components.push(component);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Update the purchase availability timeframe
|
||||
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
|
||||
self.purchase_till = purchase_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Update the active timeframe
|
||||
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
|
||||
self.active_till = active_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Check if the product is available for purchase
|
||||
pub fn is_purchasable(&self) -> bool {
|
||||
(self.status == ProductStatus::Available || self.status == ProductStatus::Active)
|
||||
&& Utc::now() <= self.purchase_till
|
||||
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||
}
|
||||
|
||||
|
||||
/// Check if the product is still active (for services)
|
||||
pub fn is_active(&self) -> bool {
|
||||
Utc::now() <= self.active_till
|
||||
}
|
||||
|
||||
/// Calculate the total cost in the specified currency
|
||||
pub fn cost_in_currency(&self, currency_code: &str) -> Option<Currency> {
|
||||
// If the price is already in the requested currency, return it
|
||||
if self.price.currency_code == currency_code {
|
||||
return Some(self.price.clone());
|
||||
}
|
||||
|
||||
// Convert the price to the requested currency
|
||||
self.price.to_currency(currency_code)
|
||||
}
|
||||
|
||||
/// Calculate the total cost in USD
|
||||
pub fn cost_in_usd(&self) -> Option<Currency> {
|
||||
self.cost_in_currency("USD")
|
||||
}
|
||||
|
||||
/// Calculate the total energy usage of the product (sum of all components)
|
||||
pub fn total_energy_usage(&self) -> f64 {
|
||||
self.components.iter().map(|c| c.total_energy_usage()).sum()
|
||||
}
|
||||
|
||||
/// Calculate the total cost of all components
|
||||
pub fn components_cost(&self, currency_code: &str) -> Option<Currency> {
|
||||
if self.components.is_empty() {
|
||||
return Some(Currency::new(0.0, currency_code.to_string()));
|
||||
}
|
||||
|
||||
// Sum up the costs of all components, converting to the requested currency
|
||||
let mut total = 0.0;
|
||||
for component in &self.components {
|
||||
let component_cost = component.total_cost();
|
||||
if let Some(converted_cost) = component_cost.to_currency(currency_code) {
|
||||
total += converted_cost.amount;
|
||||
} else {
|
||||
return None; // Conversion failed
|
||||
}
|
||||
}
|
||||
|
||||
Some(Currency::new(total, currency_code.to_string()))
|
||||
}
|
||||
|
||||
/// Calculate the total cost of all components in USD
|
||||
pub fn components_cost_in_usd(&self) -> Option<Currency> {
|
||||
self.components_cost("USD")
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Product
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
id: Option<i64>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<Currency>,
|
||||
@ -291,7 +215,7 @@ pub struct ProductBuilder {
|
||||
status: Option<ProductStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
max_amount: Option<u16>,
|
||||
max_amount: Option<i64>,
|
||||
purchase_till: Option<DateTime<Utc>>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
components: Vec<ProductComponent>,
|
||||
@ -320,7 +244,7 @@ impl ProductBuilder {
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
pub fn id(mut self, id: i64) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
@ -362,7 +286,7 @@ impl ProductBuilder {
|
||||
}
|
||||
|
||||
/// Set the max amount
|
||||
pub fn max_amount(mut self, max_amount: u16) -> Self {
|
||||
pub fn max_amount(mut self, max_amount: i64) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
@ -396,13 +320,15 @@ impl ProductBuilder {
|
||||
let now = Utc::now();
|
||||
let created_at = self.created_at.unwrap_or(now);
|
||||
let updated_at = self.updated_at.unwrap_or(now);
|
||||
|
||||
|
||||
// Calculate purchase_till and active_till based on validity_days if not set directly
|
||||
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
|
||||
let active_till = if let Some(validity_days) = self.validity_days {
|
||||
self.active_till.unwrap_or(now + Duration::days(validity_days))
|
||||
self.active_till
|
||||
.unwrap_or(now + Duration::days(validity_days))
|
||||
} else {
|
||||
self.active_till.ok_or("Either active_till or validity_days must be provided")?
|
||||
self.active_till
|
||||
.ok_or("Either active_till or validity_days must be provided")?
|
||||
};
|
||||
|
||||
Ok(Product {
|
||||
@ -438,4 +364,4 @@ impl SledModel for Product {
|
||||
}
|
||||
|
||||
// Import Currency from the currency module
|
||||
use crate::models::biz::Currency;
|
||||
use crate::models::biz::Currency;
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module // Import Sled traits from db module
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
use chrono::{DateTime, Utc};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
@ -20,9 +21,13 @@ pub struct SaleItem {
|
||||
pub sale_id: u32,
|
||||
pub product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the item
|
||||
pub comments: String, // Additional comments about the item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
pub tax_amount: Currency, // Calculated tax amount
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
}
|
||||
|
||||
@ -33,39 +38,66 @@ impl SaleItem {
|
||||
sale_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Calculate subtotal
|
||||
// Calculate subtotal (before tax)
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency {
|
||||
amount,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency {
|
||||
amount: tax_amount_value,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
sale_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total amount including tax
|
||||
pub fn total_with_tax(&self) -> Currency {
|
||||
Currency {
|
||||
amount: self.subtotal.amount + self.tax_amount.amount,
|
||||
currency_code: self.subtotal.currency_code.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SaleItem
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleItemBuilder {
|
||||
id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
@ -77,9 +109,13 @@ impl SaleItemBuilder {
|
||||
sale_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
@ -107,6 +143,18 @@ impl SaleItemBuilder {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
@ -120,6 +168,12 @@ impl SaleItemBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
@ -130,36 +184,52 @@ impl SaleItemBuilder {
|
||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency {
|
||||
amount,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency {
|
||||
amount: tax_amount_value,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
Ok(SaleItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
sale_id: self.sale_id.ok_or("sale_id is required")?,
|
||||
product_id: self.product_id.ok_or("product_id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sale represents a sale of products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Sale {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub customer_id: u32, // ID of the customer making the purchase
|
||||
pub buyer_name: String,
|
||||
pub buyer_email: String,
|
||||
pub total_amount: Currency,
|
||||
pub subtotal_amount: Currency, // Total before tax
|
||||
pub tax_amount: Currency, // Total tax
|
||||
pub total_amount: Currency, // Total including tax
|
||||
pub status: SaleStatus,
|
||||
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||
pub sale_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
@ -173,66 +243,172 @@ impl Sale {
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
customer_id: u32,
|
||||
buyer_name: String,
|
||||
buyer_email: String,
|
||||
currency_code: String,
|
||||
status: SaleStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_currency = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
customer_id,
|
||||
buyer_name,
|
||||
buyer_email,
|
||||
total_amount: Currency { amount: 0.0, currency_code },
|
||||
subtotal_amount: zero_currency.clone(),
|
||||
tax_amount: zero_currency.clone(),
|
||||
total_amount: zero_currency,
|
||||
status,
|
||||
service_id: None,
|
||||
sale_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add an item to the sale and update the total amount
|
||||
pub fn add_item(&mut self, item: SaleItem) {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
||||
|
||||
// Update the total amount
|
||||
|
||||
// Update the amounts
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency {
|
||||
// First item, initialize the amounts with the same currency
|
||||
self.subtotal_amount = Currency {
|
||||
amount: item.subtotal.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
self.tax_amount = Currency {
|
||||
amount: item.tax_amount.amount,
|
||||
currency_code: item.tax_amount.currency_code.clone(),
|
||||
};
|
||||
self.total_amount = Currency {
|
||||
amount: item.subtotal.amount + item.tax_amount.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// Add to the existing totals
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount;
|
||||
self.subtotal_amount.amount += item.subtotal.amount;
|
||||
self.tax_amount.amount += item.tax_amount.amount;
|
||||
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
|
||||
}
|
||||
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
|
||||
// Update the sale timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Recalculate all totals based on items
|
||||
pub fn recalculate_totals(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].subtotal.currency_code.clone();
|
||||
|
||||
// Calculate the totals
|
||||
let mut subtotal = 0.0;
|
||||
let mut tax_total = 0.0;
|
||||
|
||||
for item in &self.items {
|
||||
subtotal += item.subtotal.amount;
|
||||
tax_total += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Update the amounts
|
||||
self.subtotal_amount = Currency {
|
||||
amount: subtotal,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
self.tax_amount = Currency {
|
||||
amount: tax_total,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
self.total_amount = Currency {
|
||||
amount: subtotal + tax_total,
|
||||
currency_code,
|
||||
};
|
||||
|
||||
// Update the timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the sale
|
||||
pub fn update_status(&mut self, status: SaleStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Create a service from this sale
|
||||
/// This method should be called when a product of type Service is sold
|
||||
pub fn create_service(&mut self, service_id: u32, status: crate::models::biz::ServiceStatus, billing_frequency: crate::models::biz::BillingFrequency) -> Result<crate::models::biz::Service, &'static str> {
|
||||
use crate::models::biz::{Service, ServiceItem, ServiceStatus, BillingFrequency};
|
||||
|
||||
// Create a new service
|
||||
let mut service = Service::new(
|
||||
service_id,
|
||||
self.customer_id,
|
||||
self.total_amount.currency_code.clone(),
|
||||
status,
|
||||
billing_frequency,
|
||||
);
|
||||
|
||||
// Convert sale items to service items
|
||||
for sale_item in &self.items {
|
||||
// Check if the product is a service type
|
||||
// In a real implementation, you would check the product type from the database
|
||||
|
||||
// Create a service item from the sale item
|
||||
let service_item = ServiceItem::new(
|
||||
sale_item.id,
|
||||
service_id,
|
||||
sale_item.product_id,
|
||||
sale_item.name.clone(),
|
||||
sale_item.description.clone(), // Copy description from sale item
|
||||
sale_item.comments.clone(), // Copy comments from sale item
|
||||
sale_item.quantity,
|
||||
sale_item.unit_price.clone(),
|
||||
sale_item.tax_rate,
|
||||
true, // is_taxable
|
||||
sale_item.active_till,
|
||||
);
|
||||
|
||||
// Add the service item to the service
|
||||
service.add_item(service_item);
|
||||
}
|
||||
|
||||
// Link this sale to the service
|
||||
self.service_id = Some(service_id);
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
Ok(service)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Sale
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleBuilder {
|
||||
id: Option<u32>,
|
||||
company_id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
buyer_name: Option<String>,
|
||||
buyer_email: Option<String>,
|
||||
subtotal_amount: Option<Currency>,
|
||||
tax_amount: Option<Currency>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<SaleStatus>,
|
||||
service_id: Option<u32>,
|
||||
sale_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
@ -246,10 +422,14 @@ impl SaleBuilder {
|
||||
Self {
|
||||
id: None,
|
||||
company_id: None,
|
||||
customer_id: None,
|
||||
buyer_name: None,
|
||||
buyer_email: None,
|
||||
subtotal_amount: None,
|
||||
tax_amount: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
service_id: None,
|
||||
sale_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@ -269,6 +449,12 @@ impl SaleBuilder {
|
||||
self.company_id = Some(company_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_name
|
||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||
@ -293,6 +479,12 @@ impl SaleBuilder {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_date
|
||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||
@ -311,40 +503,46 @@ impl SaleBuilder {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount
|
||||
|
||||
// Initialize with empty amounts
|
||||
let mut subtotal_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
let mut tax_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
let mut total_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate total amount from items
|
||||
|
||||
// Calculate amounts from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
if item.sale_id != id {
|
||||
return Err("Item sale_id must match sale id");
|
||||
}
|
||||
|
||||
if total_amount.amount == 0.0 {
|
||||
// First item, initialize the total amount with the same currency
|
||||
total_amount = Currency {
|
||||
amount: item.subtotal.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
total_amount.amount += item.subtotal.amount;
|
||||
}
|
||||
|
||||
subtotal_amount.amount += item.subtotal.amount;
|
||||
tax_amount.amount += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||
|
||||
Ok(Sale {
|
||||
id,
|
||||
company_id: self.company_id.ok_or("company_id is required")?,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
buyer_name: self.buyer_name.ok_or("buyer_name is required")?,
|
||||
buyer_email: self.buyer_email.ok_or("buyer_email is required")?,
|
||||
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
|
||||
tax_amount: self.tax_amount.unwrap_or(tax_amount),
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_date: self.sale_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
|
@ -29,6 +29,8 @@ pub struct ServiceItem {
|
||||
pub service_id: u32,
|
||||
pub product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the service item
|
||||
pub comments: String, // Additional comments about the service item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
@ -45,6 +47,8 @@ impl ServiceItem {
|
||||
service_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
@ -76,6 +80,8 @@ impl ServiceItem {
|
||||
service_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
@ -117,6 +123,8 @@ pub struct ServiceItemBuilder {
|
||||
service_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
@ -134,6 +142,8 @@ impl ServiceItemBuilder {
|
||||
service_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
@ -167,6 +177,18 @@ impl ServiceItemBuilder {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
@ -230,6 +252,8 @@ impl ServiceItemBuilder {
|
||||
service_id: self.service_id.ok_or("service_id is required")?,
|
||||
product_id: self.product_id.ok_or("product_id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
|
@ -2,33 +2,12 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Role represents the role of a member in a circle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Stakeholder,
|
||||
Member,
|
||||
Contributor,
|
||||
Guest,
|
||||
}
|
||||
|
||||
/// Member represents a member of a circle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Member {
|
||||
pub pubkeys: Vec<String>, // public keys of the member
|
||||
pub emails: Vec<String>, // list of emails
|
||||
pub name: String, // name of the member
|
||||
pub description: String, // optional description
|
||||
pub role: Role, // role of the member in the circle
|
||||
}
|
||||
|
||||
/// Circle represents a collection of members (users or other circles)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Circle {
|
||||
pub id: u32, // unique id
|
||||
pub name: String, // name of the circle
|
||||
pub description: String, // optional description
|
||||
pub members: Vec<Member>, // members of the circle
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
@ -38,15 +17,9 @@ impl Circle {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
members: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a member to the circle
|
||||
pub fn add_member(&mut self, member: Member) {
|
||||
self.members.push(member);
|
||||
}
|
||||
|
||||
/// Returns a map of index keys for this circle
|
||||
pub fn index_keys(&self) -> HashMap<String, String> {
|
||||
let mut keys = HashMap::new();
|
||||
|
82
herodb/src/models/circle/member.rs
Normal file
82
herodb/src/models/circle/member.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Role represents the role of a member in a circle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Stakeholder,
|
||||
Member,
|
||||
Contributor,
|
||||
Guest,
|
||||
}
|
||||
|
||||
/// Member represents a member of a circle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Member {
|
||||
pub id: u32, // unique id
|
||||
pub emails: Vec<String>, // list of emails
|
||||
pub name: String, // name of the member
|
||||
pub description: String, // optional description
|
||||
pub role: Role, // role of the member in the circle
|
||||
pub contact_ids: Vec<u32>, // IDs of contacts linked to this member
|
||||
pub wallet_ids: Vec<u32>, // IDs of wallets owned by this member
|
||||
}
|
||||
|
||||
impl Member {
|
||||
/// Create a new member
|
||||
pub fn new(id: u32, name: String, description: String, role: Role) -> Self {
|
||||
Self {
|
||||
id,
|
||||
emails: Vec::new(),
|
||||
name,
|
||||
description,
|
||||
role,
|
||||
contact_ids: Vec::new(),
|
||||
wallet_ids: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an email to this member
|
||||
pub fn add_email(&mut self, email: String) {
|
||||
if !self.emails.contains(&email) {
|
||||
self.emails.push(email);
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a contact to this member
|
||||
pub fn link_contact(&mut self, contact_id: u32) {
|
||||
if !self.contact_ids.contains(&contact_id) {
|
||||
self.contact_ids.push(contact_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Link a wallet to this member
|
||||
pub fn link_wallet(&mut self, wallet_id: u32) {
|
||||
if !self.wallet_ids.contains(&wallet_id) {
|
||||
self.wallet_ids.push(wallet_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a map of index keys for this member
|
||||
pub fn index_keys(&self) -> HashMap<String, String> {
|
||||
let mut keys = HashMap::new();
|
||||
keys.insert("name".to_string(), self.name.clone());
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait (provides default dump/load)
|
||||
impl Storable for Member {}
|
||||
|
||||
// Implement SledModel trait
|
||||
impl SledModel for Member {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"member"
|
||||
}
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
pub mod circle;
|
||||
pub mod member;
|
||||
pub mod name;
|
||||
pub mod wallet;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use circle::{Circle, Member, Role};
|
||||
pub use circle::Circle;
|
||||
pub use member::{Member, Role};
|
||||
pub use name::{Name, Record, RecordType};
|
||||
pub use wallet::{Wallet, Asset};
|
||||
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};
|
||||
|
84
herodb/src/models/circle/wallet.rs
Normal file
84
herodb/src/models/circle/wallet.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Asset represents a cryptocurrency asset in a wallet
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Asset {
|
||||
pub name: String, // Asset name (e.g., "USDC")
|
||||
pub amount: f64, // Amount of the asset
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Create a new asset
|
||||
pub fn new(name: String, amount: f64) -> Self {
|
||||
Self {
|
||||
name,
|
||||
amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet represents a cryptocurrency wallet
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Wallet {
|
||||
pub id: u32, // unique id
|
||||
pub name: String, // name of the wallet
|
||||
pub description: String, // optional description
|
||||
pub blockchain_name: String, // name of the blockchain
|
||||
pub pubkey: String, // public key of the wallet
|
||||
pub assets: Vec<Asset>, // assets in the wallet
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
/// Create a new wallet
|
||||
pub fn new(id: u32, name: String, description: String, blockchain_name: String, pubkey: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
blockchain_name,
|
||||
pubkey,
|
||||
assets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an asset in the wallet (replaces if exists, adds if not)
|
||||
pub fn set_asset(&mut self, name: String, amount: f64) {
|
||||
// Check if the asset already exists
|
||||
if let Some(asset) = self.assets.iter_mut().find(|a| a.name == name) {
|
||||
// Update the amount
|
||||
asset.amount = amount;
|
||||
} else {
|
||||
// Add a new asset
|
||||
self.assets.push(Asset::new(name, amount));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total value of all assets in the wallet
|
||||
pub fn total_value(&self) -> f64 {
|
||||
self.assets.iter().map(|a| a.amount).sum()
|
||||
}
|
||||
|
||||
/// Returns a map of index keys for this wallet
|
||||
pub fn index_keys(&self) -> HashMap<String, String> {
|
||||
let mut keys = HashMap::new();
|
||||
keys.insert("name".to_string(), self.name.clone());
|
||||
keys.insert("blockchain".to_string(), self.blockchain_name.clone());
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait (provides default dump/load)
|
||||
impl Storable for Wallet {}
|
||||
|
||||
// Implement SledModel trait
|
||||
impl SledModel for Wallet {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"wallet"
|
||||
}
|
||||
}
|
496
herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md
Normal file
496
herodb/src/models/gov/GOVERNANCE_ENHANCEMENT_PLAN.md
Normal file
@ -0,0 +1,496 @@
|
||||
# Governance Module Enhancement Plan (Revised)
|
||||
|
||||
## 1. Current State Analysis
|
||||
|
||||
The governance module currently consists of:
|
||||
- **Company**: Company model with basic company information
|
||||
- **Shareholder**: Shareholder model for managing company ownership
|
||||
- **Meeting**: Meeting and Attendee models for board meetings
|
||||
- **User**: User model for system users
|
||||
- **Vote**: Vote, VoteOption, and Ballot models for voting
|
||||
|
||||
All models implement the `Storable` and `SledModel` traits for database integration, but the module has several limitations:
|
||||
- Not imported in src/models/mod.rs, making it inaccessible to the rest of the project
|
||||
- No mod.rs file to organize and re-export the types
|
||||
- No README.md file to document the purpose and usage
|
||||
- Inconsistent imports across files (e.g., crate::db vs crate::core)
|
||||
- Limited utility methods and relationships between models
|
||||
- No integration with other modules like biz, mcc, or circle
|
||||
|
||||
## 2. Planned Enhancements
|
||||
|
||||
### 2.1 Module Organization and Integration
|
||||
|
||||
- Create a mod.rs file to organize and re-export the types
|
||||
- Add the governance module to src/models/mod.rs
|
||||
- Create a README.md file to document the purpose and usage
|
||||
- Standardize imports across all files
|
||||
|
||||
### 2.2 New Models
|
||||
|
||||
#### 2.2.1 Resolution Model
|
||||
|
||||
Create a new `resolution.rs` file with a Resolution model for managing board resolutions:
|
||||
- Resolution information (title, description, text)
|
||||
- Resolution status (Draft, Proposed, Approved, Rejected)
|
||||
- Voting results and approvals
|
||||
- Integration with Meeting and Vote models
|
||||
|
||||
### 2.3 Enhanced Relationships and Integration
|
||||
|
||||
#### 2.3.1 Integration with Biz Module
|
||||
|
||||
- Link Company with biz::Customer and biz::Contract
|
||||
- Link Shareholder with biz::Customer
|
||||
- Link Meeting with biz::Invoice for expense tracking
|
||||
|
||||
#### 2.3.2 Integration with MCC Module
|
||||
|
||||
- Link Meeting with mcc::Calendar and mcc::Event
|
||||
- Link User with mcc::Contact
|
||||
- Link Vote with mcc::Message for notifications
|
||||
|
||||
#### 2.3.3 Integration with Circle Module
|
||||
|
||||
- Link Company with circle::Circle for group-based access control
|
||||
- Link User with circle::Member for role-based permissions
|
||||
|
||||
### 2.4 Utility Methods and Functionality
|
||||
|
||||
- Add filtering and searching methods to all models
|
||||
- Add relationship management methods between models
|
||||
- Add validation and business logic methods
|
||||
|
||||
## 3. Implementation Plan
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Review Current Models] --> B[Create mod.rs and Update models/mod.rs]
|
||||
B --> C[Standardize Imports and Fix Inconsistencies]
|
||||
C --> D[Create Resolution Model]
|
||||
D --> E[Implement Integration with Other Modules]
|
||||
E --> F[Add Utility Methods]
|
||||
F --> G[Create README.md and Documentation]
|
||||
G --> H[Write Tests]
|
||||
```
|
||||
|
||||
### 3.1 Detailed Changes
|
||||
|
||||
#### 3.1.1 Module Organization
|
||||
|
||||
Create a new `mod.rs` file in the governance directory:
|
||||
|
||||
```rust
|
||||
pub mod company;
|
||||
pub mod shareholder;
|
||||
pub mod meeting;
|
||||
pub mod user;
|
||||
pub mod vote;
|
||||
pub mod resolution;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub use shareholder::{Shareholder, ShareholderType};
|
||||
pub use meeting::{Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus};
|
||||
pub use user::User;
|
||||
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||
pub use resolution::{Resolution, ResolutionStatus, Approval};
|
||||
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};
|
||||
```
|
||||
|
||||
Update `src/models/mod.rs` to include the governance module:
|
||||
|
||||
```rust
|
||||
pub mod biz;
|
||||
pub mod mcc;
|
||||
pub mod circle;
|
||||
pub mod governance;
|
||||
```
|
||||
|
||||
#### 3.1.2 Resolution Model (`resolution.rs`)
|
||||
|
||||
```rust
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
||||
use crate::models::gov::{Meeting, Vote};
|
||||
|
||||
/// ResolutionStatus represents the status of a resolution
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ResolutionStatus {
|
||||
Draft,
|
||||
Proposed,
|
||||
Approved,
|
||||
Rejected,
|
||||
Withdrawn,
|
||||
}
|
||||
|
||||
/// Resolution represents a board resolution
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Resolution {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub meeting_id: Option<u32>,
|
||||
pub vote_id: Option<u32>,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub text: String,
|
||||
pub status: ResolutionStatus,
|
||||
pub proposed_by: u32, // User ID
|
||||
pub proposed_at: DateTime<Utc>,
|
||||
pub approved_at: Option<DateTime<Utc>>,
|
||||
pub rejected_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub approvals: Vec<Approval>,
|
||||
}
|
||||
|
||||
/// Approval represents an approval of a resolution by a board member
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Approval {
|
||||
pub id: u32,
|
||||
pub resolution_id: u32,
|
||||
pub user_id: u32,
|
||||
pub name: String,
|
||||
pub approved: bool,
|
||||
pub comments: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
/// Create a new resolution with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
text: String,
|
||||
proposed_by: u32,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
meeting_id: None,
|
||||
vote_id: None,
|
||||
title,
|
||||
description,
|
||||
text,
|
||||
status: ResolutionStatus::Draft,
|
||||
proposed_by,
|
||||
proposed_at: now,
|
||||
approved_at: None,
|
||||
rejected_at: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
approvals: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Propose the resolution
|
||||
pub fn propose(&mut self) {
|
||||
self.status = ResolutionStatus::Proposed;
|
||||
self.proposed_at = Utc::now();
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Approve the resolution
|
||||
pub fn approve(&mut self) {
|
||||
self.status = ResolutionStatus::Approved;
|
||||
self.approved_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Reject the resolution
|
||||
pub fn reject(&mut self) {
|
||||
self.status = ResolutionStatus::Rejected;
|
||||
self.rejected_at = Some(Utc::now());
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Add an approval to the resolution
|
||||
pub fn add_approval(&mut self, user_id: u32, name: String, approved: bool, comments: String) -> &Approval {
|
||||
let id = if self.approvals.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.approvals.iter().map(|a| a.id).max().unwrap_or(0) + 1
|
||||
};
|
||||
|
||||
let approval = Approval {
|
||||
id,
|
||||
resolution_id: self.id,
|
||||
user_id,
|
||||
name,
|
||||
approved,
|
||||
comments,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
|
||||
self.approvals.push(approval);
|
||||
self.updated_at = Utc::now();
|
||||
self.approvals.last().unwrap()
|
||||
}
|
||||
|
||||
/// Link this resolution to a meeting
|
||||
pub fn link_to_meeting(&mut self, meeting_id: u32) {
|
||||
self.meeting_id = Some(meeting_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link this resolution to a vote
|
||||
pub fn link_to_vote(&mut self, vote_id: u32) {
|
||||
self.vote_id = Some(vote_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait (provides default dump/load)
|
||||
impl Storable for Resolution {}
|
||||
impl Storable for Approval {}
|
||||
|
||||
// Implement SledModel trait
|
||||
impl SledModel for Resolution {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"resolution"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.3 Enhanced Company Model (`company.rs`)
|
||||
|
||||
Add integration with other modules:
|
||||
|
||||
```rust
|
||||
impl Company {
|
||||
// ... existing methods ...
|
||||
|
||||
/// Link this company to a Circle for access control
|
||||
pub fn link_to_circle(&mut self, circle_id: u32) -> Result<(), SledDBError> {
|
||||
// Implementation details
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Link this company to a Customer in the biz module
|
||||
pub fn link_to_customer(&mut self, customer_id: u32) -> Result<(), SledDBError> {
|
||||
// Implementation details
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all resolutions for this company
|
||||
pub fn get_resolutions(&self, db: &SledDB<Resolution>) -> Result<Vec<Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
let company_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.company_id == self.id)
|
||||
.collect();
|
||||
|
||||
Ok(company_resolutions)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.4 Enhanced Meeting Model (`meeting.rs`)
|
||||
|
||||
Add integration with other modules:
|
||||
|
||||
```rust
|
||||
impl Meeting {
|
||||
// ... existing methods ...
|
||||
|
||||
/// Link this meeting to a Calendar Event in the mcc module
|
||||
pub fn link_to_event(&mut self, event_id: u32) -> Result<(), SledDBError> {
|
||||
// Implementation details
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all resolutions discussed in this meeting
|
||||
pub fn get_resolutions(&self, db: &SledDB<Resolution>) -> Result<Vec<Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
let meeting_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.meeting_id == Some(self.id))
|
||||
.collect();
|
||||
|
||||
Ok(meeting_resolutions)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.5 Enhanced Vote Model (`vote.rs`)
|
||||
|
||||
Add integration with Resolution model:
|
||||
|
||||
```rust
|
||||
impl Vote {
|
||||
// ... existing methods ...
|
||||
|
||||
/// Get the resolution associated with this vote
|
||||
pub fn get_resolution(&self, db: &SledDB<Resolution>) -> Result<Option<Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
let vote_resolution = all_resolutions
|
||||
.into_iter()
|
||||
.find(|resolution| resolution.vote_id == Some(self.id));
|
||||
|
||||
Ok(vote_resolution)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.6 Create README.md
|
||||
|
||||
Create a README.md file to document the purpose and usage of the governance module.
|
||||
|
||||
## 4. Data Model Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Company {
|
||||
+u32 id
|
||||
+String name
|
||||
+String registration_number
|
||||
+DateTime incorporation_date
|
||||
+String fiscal_year_end
|
||||
+String email
|
||||
+String phone
|
||||
+String website
|
||||
+String address
|
||||
+BusinessType business_type
|
||||
+String industry
|
||||
+String description
|
||||
+CompanyStatus status
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+add_shareholder()
|
||||
+link_to_circle()
|
||||
+link_to_customer()
|
||||
+get_resolutions()
|
||||
}
|
||||
|
||||
class Shareholder {
|
||||
+u32 id
|
||||
+u32 company_id
|
||||
+u32 user_id
|
||||
+String name
|
||||
+f64 shares
|
||||
+f64 percentage
|
||||
+ShareholderType type_
|
||||
+DateTime since
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+update_shares()
|
||||
}
|
||||
|
||||
class Meeting {
|
||||
+u32 id
|
||||
+u32 company_id
|
||||
+String title
|
||||
+DateTime date
|
||||
+String location
|
||||
+String description
|
||||
+MeetingStatus status
|
||||
+String minutes
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+Vec~Attendee~ attendees
|
||||
+add_attendee()
|
||||
+update_status()
|
||||
+update_minutes()
|
||||
+find_attendee_by_user_id()
|
||||
+confirmed_attendees()
|
||||
+link_to_event()
|
||||
+get_resolutions()
|
||||
}
|
||||
|
||||
class User {
|
||||
+u32 id
|
||||
+String name
|
||||
+String email
|
||||
+String password
|
||||
+String company
|
||||
+String role
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
}
|
||||
|
||||
class Vote {
|
||||
+u32 id
|
||||
+u32 company_id
|
||||
+String title
|
||||
+String description
|
||||
+DateTime start_date
|
||||
+DateTime end_date
|
||||
+VoteStatus status
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+Vec~VoteOption~ options
|
||||
+Vec~Ballot~ ballots
|
||||
+Vec~u32~ private_group
|
||||
+add_option()
|
||||
+add_ballot()
|
||||
+get_resolution()
|
||||
}
|
||||
|
||||
class Resolution {
|
||||
+u32 id
|
||||
+u32 company_id
|
||||
+Option~u32~ meeting_id
|
||||
+Option~u32~ vote_id
|
||||
+String title
|
||||
+String description
|
||||
+String text
|
||||
+ResolutionStatus status
|
||||
+u32 proposed_by
|
||||
+DateTime proposed_at
|
||||
+Option~DateTime~ approved_at
|
||||
+Option~DateTime~ rejected_at
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+Vec~Approval~ approvals
|
||||
+propose()
|
||||
+approve()
|
||||
+reject()
|
||||
+add_approval()
|
||||
+link_to_meeting()
|
||||
+link_to_vote()
|
||||
}
|
||||
|
||||
Company "1" -- "many" Shareholder: has
|
||||
Company "1" -- "many" Meeting: holds
|
||||
Company "1" -- "many" Vote: conducts
|
||||
Company "1" -- "many" Resolution: issues
|
||||
Meeting "1" -- "many" Attendee: has
|
||||
Meeting "1" -- "many" Resolution: discusses
|
||||
Vote "1" -- "many" VoteOption: has
|
||||
Vote "1" -- "many" Ballot: collects
|
||||
Vote "1" -- "1" Resolution: decides
|
||||
Resolution "1" -- "many" Approval: receives
|
||||
```
|
||||
|
||||
## 5. Testing Strategy
|
||||
|
||||
1. Unit tests for each model to verify:
|
||||
- Basic functionality
|
||||
- Serialization/deserialization
|
||||
- Utility methods
|
||||
- Integration with other models
|
||||
2. Integration tests to verify:
|
||||
- Database operations with the models
|
||||
- Relationships between models
|
||||
- Integration with other modules
|
||||
|
||||
## 6. Future Considerations
|
||||
|
||||
1. **Committee Model**: Add a Committee model in the future if needed
|
||||
2. **Compliance Model**: Add compliance-related models in the future if needed
|
||||
3. **API Integration**: Develop REST API endpoints for the governance module
|
||||
4. **UI Components**: Create UI components for managing governance entities
|
||||
5. **Reporting**: Implement reporting functionality for governance metrics
|
@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
||||
use crate::models::governance::User;
|
||||
use crate::models::gov::User;
|
||||
|
||||
/// CommitteeRole represents the role of a member in a committee
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
@ -167,8 +167,8 @@ impl Company {
|
||||
}
|
||||
|
||||
/// Get all resolutions for this company
|
||||
pub fn get_resolutions(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Vec<crate::models::governance::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
let company_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.company_id == self.id)
|
212
herodb/src/models/gov/compliance.rs
Normal file
212
herodb/src/models/gov/compliance.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
||||
use crate::models::gov::Company;
|
||||
|
||||
/// ComplianceRequirement represents a regulatory requirement
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ComplianceRequirement {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub regulation: String,
|
||||
pub authority: String,
|
||||
pub deadline: DateTime<Utc>,
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// ComplianceDocument represents a compliance document
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ComplianceDocument {
|
||||
pub id: u32,
|
||||
pub requirement_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub file_path: String,
|
||||
pub file_type: String,
|
||||
pub uploaded_by: u32, // User ID
|
||||
pub uploaded_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// ComplianceAudit represents a compliance audit
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ComplianceAudit {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub auditor: String,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub status: String,
|
||||
pub findings: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ComplianceRequirement {
|
||||
/// Create a new compliance requirement with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
regulation: String,
|
||||
authority: String,
|
||||
deadline: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
title,
|
||||
description,
|
||||
regulation,
|
||||
authority,
|
||||
deadline,
|
||||
status: "Pending".to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the requirement
|
||||
pub fn update_status(&mut self, status: String) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Get the company associated with this requirement
|
||||
pub fn get_company(&self, db: &SledDB<Company>) -> Result<Company, SledDBError> {
|
||||
db.get(&self.company_id.to_string())
|
||||
}
|
||||
|
||||
/// Get all documents associated with this requirement
|
||||
pub fn get_documents(&self, db: &SledDB<ComplianceDocument>) -> Result<Vec<ComplianceDocument>, SledDBError> {
|
||||
let all_documents = db.list()?;
|
||||
let requirement_documents = all_documents
|
||||
.into_iter()
|
||||
.filter(|doc| doc.requirement_id == self.id)
|
||||
.collect();
|
||||
|
||||
Ok(requirement_documents)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComplianceDocument {
|
||||
/// Create a new compliance document with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
requirement_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
file_path: String,
|
||||
file_type: String,
|
||||
uploaded_by: u32,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
requirement_id,
|
||||
title,
|
||||
description,
|
||||
file_path,
|
||||
file_type,
|
||||
uploaded_by,
|
||||
uploaded_at: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the requirement associated with this document
|
||||
pub fn get_requirement(&self, db: &SledDB<ComplianceRequirement>) -> Result<ComplianceRequirement, SledDBError> {
|
||||
db.get(&self.requirement_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ComplianceAudit {
|
||||
/// Create a new compliance audit with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
auditor: String,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
title,
|
||||
description,
|
||||
auditor,
|
||||
start_date,
|
||||
end_date,
|
||||
status: "Planned".to_string(),
|
||||
findings: String::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the audit
|
||||
pub fn update_status(&mut self, status: String) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the findings of the audit
|
||||
pub fn update_findings(&mut self, findings: String) {
|
||||
self.findings = findings;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Get the company associated with this audit
|
||||
pub fn get_company(&self, db: &SledDB<Company>) -> Result<Company, SledDBError> {
|
||||
db.get(&self.company_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait (provides default dump/load)
|
||||
impl Storable for ComplianceRequirement {}
|
||||
impl Storable for ComplianceDocument {}
|
||||
impl Storable for ComplianceAudit {}
|
||||
|
||||
// Implement SledModel trait
|
||||
impl SledModel for ComplianceRequirement {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"compliance_requirement"
|
||||
}
|
||||
}
|
||||
|
||||
impl SledModel for ComplianceDocument {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"compliance_document"
|
||||
}
|
||||
}
|
||||
|
||||
impl SledModel for ComplianceAudit {
|
||||
fn get_id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"compliance_audit"
|
||||
}
|
||||
}
|
@ -164,8 +164,8 @@ impl Meeting {
|
||||
}
|
||||
|
||||
/// Get all resolutions discussed in this meeting
|
||||
pub fn get_resolutions(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Vec<crate::models::governance::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
let meeting_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.meeting_id == Some(self.id))
|
@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
||||
use crate::models::governance::{Meeting, Vote};
|
||||
use crate::models::gov::{Meeting, Vote};
|
||||
|
||||
/// ResolutionStatus represents the status of a resolution
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -158,10 +158,10 @@ impl Resolution {
|
||||
}
|
||||
|
||||
/// Get the meeting associated with this resolution
|
||||
pub fn get_meeting(&self, db: &SledDB<Meeting>) -> Result<Option<Meeting>, SledDBError> {
|
||||
pub fn get_meeting(&self, db: &crate::db::DB) -> Result<Option<Meeting>, SledDBError> {
|
||||
match self.meeting_id {
|
||||
Some(meeting_id) => {
|
||||
let meeting = db.get(&meeting_id.to_string())?;
|
||||
let meeting = db.get::<Meeting>(&meeting_id.to_string())?;
|
||||
Ok(Some(meeting))
|
||||
}
|
||||
None => Ok(None),
|
||||
@ -169,10 +169,10 @@ impl Resolution {
|
||||
}
|
||||
|
||||
/// Get the vote associated with this resolution
|
||||
pub fn get_vote(&self, db: &SledDB<Vote>) -> Result<Option<Vote>, SledDBError> {
|
||||
pub fn get_vote(&self, db: &crate::db::DB) -> Result<Option<Vote>, SledDBError> {
|
||||
match self.vote_id {
|
||||
Some(vote_id) => {
|
||||
let vote = db.get(&vote_id.to_string())?;
|
||||
let vote = db.get::<Vote>(&vote_id.to_string())?;
|
||||
Ok(Some(vote))
|
||||
}
|
||||
None => Ok(None),
|
@ -128,8 +128,8 @@ impl Vote {
|
||||
}
|
||||
|
||||
/// Get the resolution associated with this vote
|
||||
pub fn get_resolution(&self, db: &SledDB<crate::models::governance::Resolution>) -> Result<Option<crate::models::governance::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list()?;
|
||||
pub fn get_resolution(&self, db: &crate::db::DB) -> Result<Option<super::Resolution>, SledDBError> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
let vote_resolution = all_resolutions
|
||||
.into_iter()
|
||||
.find(|resolution| resolution.vote_id == Some(self.id));
|
18
herodb/src/models/instructions.md
Normal file
18
herodb/src/models/instructions.md
Normal file
@ -0,0 +1,18 @@
|
||||
in @src/models/circle/circle.rs
|
||||
|
||||
- member us now new rootobject, check implementation
|
||||
- a member is linked to one or more contacts id's (from src/models/mcc/contacts.rs)
|
||||
- a member has one or more wallets
|
||||
|
||||
|
||||
in@src/models/biz add a ticket module
|
||||
|
||||
user can have more than 1 ticket which is to ask support from the org
|
||||
|
||||
a ticket has following fields
|
||||
|
||||
- subject
|
||||
- description
|
||||
- creation/update date
|
||||
- assignees (based on memberid see above)
|
||||
-
|
@ -1,4 +1,4 @@
|
||||
pub mod biz;
|
||||
pub mod mcc;
|
||||
pub mod circle;
|
||||
pub mod governance;
|
||||
pub mod gov;
|
131
herodb/src/models/py/README.md
Normal file
131
herodb/src/models/py/README.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Business Models Python Port
|
||||
|
||||
This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration.
|
||||
|
||||
## Overview
|
||||
|
||||
This project includes:
|
||||
|
||||
1. Python port of Rust business models using SQLModel
|
||||
2. FastAPI server with OpenAPI/Swagger documentation
|
||||
3. CRUD operations for all models
|
||||
4. Convenience endpoints for common operations
|
||||
|
||||
The models ported from Rust to Python include:
|
||||
|
||||
- **Currency**: Represents a monetary value with amount and currency code
|
||||
- **Customer**: Represents a customer who can purchase products or services
|
||||
- **Product**: Represents a product or service offered
|
||||
- **ProductComponent**: Represents a component of a product
|
||||
- **SaleItem**: Represents an item in a sale
|
||||
- **Sale**: Represents a sale of products or services
|
||||
|
||||
## Structure
|
||||
|
||||
- `models.py`: Contains the SQLModel definitions for all business models
|
||||
- `example.py`: Demonstrates how to use the models with a sample application
|
||||
- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example
|
||||
- `api.py`: FastAPI server providing CRUD operations for all models
|
||||
- `server.sh`: Bash script to start the FastAPI server
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- [uv](https://github.com/astral-sh/uv) for dependency management
|
||||
|
||||
## Installation
|
||||
|
||||
The project uses `uv` for dependency management. To install dependencies and run the example:
|
||||
|
||||
```bash
|
||||
./install_and_run.sh
|
||||
```
|
||||
|
||||
## API Server
|
||||
|
||||
The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints.
|
||||
|
||||
### Starting the Server
|
||||
|
||||
To start the API server:
|
||||
|
||||
```bash
|
||||
./server.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Create a virtual environment if it doesn't exist
|
||||
2. Install the required dependencies using `uv`
|
||||
3. Start the FastAPI server with hot reloading enabled
|
||||
|
||||
### API Documentation
|
||||
|
||||
Once the server is running, you can access the OpenAPI documentation at:
|
||||
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
|
||||
### Available Endpoints
|
||||
|
||||
The API provides the following endpoints:
|
||||
|
||||
#### Currencies
|
||||
- `GET /currencies/`: List all currencies
|
||||
- `POST /currencies/`: Create a new currency
|
||||
- `GET /currencies/{currency_id}`: Get a specific currency
|
||||
- `PUT /currencies/{currency_id}`: Update a currency
|
||||
- `DELETE /currencies/{currency_id}`: Delete a currency
|
||||
|
||||
#### Customers
|
||||
- `GET /customers/`: List all customers
|
||||
- `POST /customers/`: Create a new customer
|
||||
- `GET /customers/{customer_id}`: Get a specific customer
|
||||
- `PUT /customers/{customer_id}`: Update a customer
|
||||
- `DELETE /customers/{customer_id}`: Delete a customer
|
||||
- `GET /customers/{customer_id}/sales/`: Get all sales for a customer
|
||||
|
||||
#### Products
|
||||
- `GET /products/`: List all products
|
||||
- `POST /products/`: Create a new product
|
||||
- `GET /products/{product_id}`: Get a specific product
|
||||
- `PUT /products/{product_id}`: Update a product
|
||||
- `DELETE /products/{product_id}`: Delete a product
|
||||
- `GET /products/available/`: Get all available products
|
||||
- `POST /products/{product_id}/components/`: Add a component to a product
|
||||
- `GET /products/{product_id}/components/`: Get all components for a product
|
||||
|
||||
#### Sales
|
||||
- `GET /sales/`: List all sales
|
||||
- `POST /sales/`: Create a new sale
|
||||
- `GET /sales/{sale_id}`: Get a specific sale
|
||||
- `PUT /sales/{sale_id}`: Update a sale
|
||||
- `DELETE /sales/{sale_id}`: Delete a sale
|
||||
- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale
|
||||
- `POST /sales/{sale_id}/items/`: Add an item to a sale
|
||||
- `GET /sales/{sale_id}/items/`: Get all items for a sale
|
||||
|
||||
## Dependencies
|
||||
|
||||
- SQLModel: For database models and ORM functionality
|
||||
- Pydantic: For data validation (used by SQLModel)
|
||||
- FastAPI: For creating the API server
|
||||
- Uvicorn: ASGI server for running FastAPI applications
|
||||
|
||||
## Example Usage
|
||||
|
||||
The `example.py` script demonstrates:
|
||||
|
||||
1. Creating an SQLite database
|
||||
2. Defining and creating tables for the models
|
||||
3. Creating sample data (customers, products, sales)
|
||||
4. Performing operations on the data
|
||||
5. Querying and displaying the data
|
||||
|
||||
To run the example manually (after activating the virtual environment):
|
||||
|
||||
```bash
|
||||
# From the py directory
|
||||
python example.py
|
||||
|
||||
# Or from the parent directory
|
||||
cd py && python example.py
|
3
herodb/src/models/py/__init__.py
Normal file
3
herodb/src/models/py/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""
|
||||
Python port of the business models from Rust.
|
||||
"""
|
BIN
herodb/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
BIN
herodb/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
herodb/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
BIN
herodb/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
455
herodb/src/models/py/api.py
Executable file
455
herodb/src/models/py/api.py
Executable file
@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI server providing CRUD operations for business models.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import (
|
||||
Currency,
|
||||
Customer,
|
||||
Product,
|
||||
ProductComponent,
|
||||
ProductStatus,
|
||||
ProductType,
|
||||
Sale,
|
||||
SaleItem,
|
||||
SaleStatus,
|
||||
)
|
||||
|
||||
# Create database
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
|
||||
# Create tables
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Business API",
|
||||
description="API for business models",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependency to get database session
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to the Business API"}
|
||||
|
||||
|
||||
# Currency endpoints
|
||||
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
|
||||
def create_currency(currency: Currency, session: Session = Depends(get_session)):
|
||||
"""Create a new currency"""
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
|
||||
def read_currencies(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all currencies"""
|
||||
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
|
||||
return currencies
|
||||
|
||||
|
||||
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def read_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a currency by ID"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
return currency
|
||||
|
||||
|
||||
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def update_currency(
|
||||
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
# Update currency attributes
|
||||
currency_data_dict = currency_data.dict(exclude_unset=True)
|
||||
for key, value in currency_data_dict.items():
|
||||
setattr(currency, key, value)
|
||||
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
|
||||
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
session.delete(currency)
|
||||
session.commit()
|
||||
return {"message": "Currency deleted successfully"}
|
||||
|
||||
|
||||
# Customer endpoints
|
||||
@app.post("/customers/", response_model=Customer, tags=["Customers"])
|
||||
def create_customer(customer: Customer, session: Session = Depends(get_session)):
|
||||
"""Create a new customer"""
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
|
||||
def read_customers(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all customers"""
|
||||
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
|
||||
return customers
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def read_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a customer by ID"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def update_customer(
|
||||
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Update customer attributes
|
||||
customer_data_dict = customer_data.dict(exclude_unset=True)
|
||||
for key, value in customer_data_dict.items():
|
||||
setattr(customer, key, value)
|
||||
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.delete("/customers/{customer_id}", tags=["Customers"])
|
||||
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.delete(customer)
|
||||
session.commit()
|
||||
return {"message": "Customer deleted successfully"}
|
||||
|
||||
|
||||
# Product endpoints
|
||||
@app.post("/products/", response_model=Product, tags=["Products"])
|
||||
def create_product(product: Product, session: Session = Depends(get_session)):
|
||||
"""Create a new product"""
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.get("/products/", response_model=List[Product], tags=["Products"])
|
||||
def read_products(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category: Optional[str] = None,
|
||||
status: Optional[ProductStatus] = None,
|
||||
istemplate: Optional[bool] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all products with optional filtering"""
|
||||
query = select(Product)
|
||||
|
||||
if category:
|
||||
query = query.where(Product.category == category)
|
||||
|
||||
if status:
|
||||
query = query.where(Product.status == status)
|
||||
|
||||
if istemplate is not None:
|
||||
query = query.where(Product.istemplate == istemplate)
|
||||
|
||||
products = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def read_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a product by ID"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return product
|
||||
|
||||
|
||||
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def update_product(
|
||||
product_id: int, product_data: Product, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# Update product attributes
|
||||
product_data_dict = product_data.dict(exclude_unset=True)
|
||||
for key, value in product_data_dict.items():
|
||||
setattr(product, key, value)
|
||||
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.delete("/products/{product_id}", tags=["Products"])
|
||||
def delete_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
return {"message": "Product deleted successfully"}
|
||||
|
||||
|
||||
# Product Component endpoints
|
||||
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
|
||||
def create_product_component(
|
||||
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add a component to a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
component.product_id = product_id
|
||||
session.add(component)
|
||||
session.commit()
|
||||
session.refresh(component)
|
||||
return component
|
||||
|
||||
|
||||
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
|
||||
def read_product_components(
|
||||
product_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all components for a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
return product.components
|
||||
|
||||
|
||||
# Sale endpoints
|
||||
@app.post("/sales/", response_model=Sale, tags=["Sales"])
|
||||
def create_sale(sale: Sale, session: Session = Depends(get_session)):
|
||||
"""Create a new sale"""
|
||||
# Ensure customer exists if customer_id is provided
|
||||
if sale.customer_id:
|
||||
customer = session.get(Customer, sale.customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
|
||||
def read_sales(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[SaleStatus] = None,
|
||||
customer_id: Optional[int] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales with optional filtering"""
|
||||
query = select(Sale)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
if customer_id:
|
||||
query = query.where(Sale.customer_id == customer_id)
|
||||
|
||||
sales = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return sales
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def read_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a sale by ID"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
return sale
|
||||
|
||||
|
||||
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def update_sale(
|
||||
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
# Update sale attributes
|
||||
sale_data_dict = sale_data.dict(exclude_unset=True)
|
||||
for key, value in sale_data_dict.items():
|
||||
setattr(sale, key, value)
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.delete("/sales/{sale_id}", tags=["Sales"])
|
||||
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
session.delete(sale)
|
||||
session.commit()
|
||||
return {"message": "Sale deleted successfully"}
|
||||
|
||||
|
||||
# Sale Item endpoints
|
||||
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
|
||||
def create_sale_item(
|
||||
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add an item to a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
item.sale_id = sale_id
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
# Update the sale's total amount
|
||||
sale.add_item(item)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
|
||||
return item
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
|
||||
def read_sale_items(
|
||||
sale_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all items for a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
return sale.items
|
||||
|
||||
|
||||
# Convenience endpoints
|
||||
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
|
||||
def update_sale_status(
|
||||
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update the status of a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
sale.update_status(status)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
|
||||
def get_available_products(
|
||||
istemplate: Optional[bool] = False,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all available products"""
|
||||
query = select(Product).where(
|
||||
Product.status == ProductStatus.AVAILABLE,
|
||||
Product.purchase_till > datetime.utcnow(),
|
||||
Product.istemplate == istemplate
|
||||
)
|
||||
products = session.exec(query).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
|
||||
def get_customer_sales(
|
||||
customer_id: int,
|
||||
status: Optional[SaleStatus] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales for a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
query = select(Sale).where(Sale.customer_id == customer_id)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
sales = session.exec(query).all()
|
||||
return sales
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
BIN
herodb/src/models/py/business.db
Normal file
BIN
herodb/src/models/py/business.db
Normal file
Binary file not shown.
190
herodb/src/models/py/example.py
Executable file
190
herodb/src/models/py/example.py
Executable file
@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example script demonstrating the use of the business models.
|
||||
"""
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import (
|
||||
Currency,
|
||||
Customer,
|
||||
Product,
|
||||
ProductComponent,
|
||||
ProductStatus,
|
||||
ProductType,
|
||||
Sale,
|
||||
SaleItem,
|
||||
SaleStatus,
|
||||
)
|
||||
|
||||
|
||||
def create_tables(engine):
|
||||
"""Create all tables in the database"""
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
def create_sample_data(session: Session) -> None:
|
||||
"""Create sample data for demonstration"""
|
||||
# Create currencies
|
||||
usd = Currency(currency_code="USD", amount=0.0)
|
||||
eur = Currency(currency_code="EUR", amount=0.0)
|
||||
session.add(usd)
|
||||
session.add(eur)
|
||||
session.commit()
|
||||
|
||||
# Create a customer
|
||||
customer = Customer.new(
|
||||
name="Acme Corporation",
|
||||
description="A fictional company",
|
||||
pubkey="acme123456",
|
||||
contact_sids=["circle1_contact123", "circle2_contact456"]
|
||||
)
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
|
||||
# Create product components
|
||||
cpu_component = ProductComponent.new(
|
||||
name="CPU",
|
||||
description="Central Processing Unit",
|
||||
quantity=1,
|
||||
)
|
||||
ram_component = ProductComponent.new(
|
||||
name="RAM",
|
||||
description="Random Access Memory",
|
||||
quantity=2,
|
||||
)
|
||||
session.add(cpu_component)
|
||||
session.add(ram_component)
|
||||
session.commit()
|
||||
|
||||
# Create products
|
||||
laptop_price = Currency(currency_code="USD", amount=1200.0)
|
||||
session.add(laptop_price)
|
||||
session.commit()
|
||||
|
||||
laptop = Product.new(
|
||||
name="Laptop",
|
||||
description="High-performance laptop",
|
||||
price=laptop_price,
|
||||
type_=ProductType.PRODUCT,
|
||||
category="Electronics",
|
||||
status=ProductStatus.AVAILABLE,
|
||||
max_amount=100,
|
||||
validity_days=365,
|
||||
istemplate=False,
|
||||
)
|
||||
laptop.add_component(cpu_component)
|
||||
laptop.add_component(ram_component)
|
||||
session.add(laptop)
|
||||
session.commit()
|
||||
|
||||
support_price = Currency(currency_code="USD", amount=50.0)
|
||||
session.add(support_price)
|
||||
session.commit()
|
||||
|
||||
support = Product.new(
|
||||
name="Technical Support",
|
||||
description="24/7 technical support",
|
||||
price=support_price,
|
||||
type_=ProductType.SERVICE,
|
||||
category="Support",
|
||||
status=ProductStatus.AVAILABLE,
|
||||
max_amount=1000,
|
||||
validity_days=30,
|
||||
istemplate=True, # This is a template product
|
||||
)
|
||||
session.add(support)
|
||||
session.commit()
|
||||
|
||||
# Create a sale
|
||||
sale = Sale.new(
|
||||
customer=customer,
|
||||
currency_code="USD",
|
||||
)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
|
||||
# Create sale items
|
||||
laptop_unit_price = Currency(currency_code="USD", amount=1200.0)
|
||||
session.add(laptop_unit_price)
|
||||
session.commit()
|
||||
|
||||
laptop_item = SaleItem.new(
|
||||
product=laptop,
|
||||
quantity=1,
|
||||
unit_price=laptop_unit_price,
|
||||
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=365),
|
||||
)
|
||||
sale.add_item(laptop_item)
|
||||
|
||||
support_unit_price = Currency(currency_code="USD", amount=50.0)
|
||||
session.add(support_unit_price)
|
||||
session.commit()
|
||||
|
||||
support_item = SaleItem.new(
|
||||
product=support,
|
||||
quantity=2,
|
||||
unit_price=support_unit_price,
|
||||
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||
)
|
||||
sale.add_item(support_item)
|
||||
|
||||
# Complete the sale
|
||||
sale.update_status(SaleStatus.COMPLETED)
|
||||
session.commit()
|
||||
|
||||
|
||||
def query_data(session: Session) -> None:
|
||||
"""Query and display data from the database"""
|
||||
print("\n=== Customers ===")
|
||||
customers = session.exec(select(Customer)).all()
|
||||
for customer in customers:
|
||||
print(f"Customer: {customer.name} ({customer.pubkey})")
|
||||
print(f" Description: {customer.description}")
|
||||
print(f" Contact SIDs: {', '.join(customer.contact_sids)}")
|
||||
print(f" Created at: {customer.created_at}")
|
||||
|
||||
print("\n=== Products ===")
|
||||
products = session.exec(select(Product)).all()
|
||||
for product in products:
|
||||
print(f"Product: {product.name} ({product.type_.value})")
|
||||
print(f" Description: {product.description}")
|
||||
print(f" Price: {product.price.amount} {product.price.currency_code}")
|
||||
print(f" Status: {product.status.value}")
|
||||
print(f" Is Template: {product.istemplate}")
|
||||
print(f" Components:")
|
||||
for component in product.components:
|
||||
print(f" - {component.name}: {component.quantity}")
|
||||
|
||||
print("\n=== Sales ===")
|
||||
sales = session.exec(select(Sale)).all()
|
||||
for sale in sales:
|
||||
print(f"Sale to: {sale.customer.name}")
|
||||
print(f" Status: {sale.status.value}")
|
||||
print(f" Total: {sale.total_amount.amount} {sale.total_amount.currency_code}")
|
||||
print(f" Items:")
|
||||
for item in sale.items:
|
||||
print(f" - {item.name}: {item.quantity} x {item.unit_price.amount} = {item.subtotal.amount} {item.subtotal.currency_code}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("Creating in-memory SQLite database...")
|
||||
engine = create_engine("sqlite:///business.db", echo=False)
|
||||
|
||||
print("Creating tables...")
|
||||
create_tables(engine)
|
||||
|
||||
print("Creating sample data...")
|
||||
with Session(engine) as session:
|
||||
create_sample_data(session)
|
||||
|
||||
print("Querying data...")
|
||||
with Session(engine) as session:
|
||||
query_data(session)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
49
herodb/src/models/py/install_and_run.sh
Executable file
49
herodb/src/models/py/install_and_run.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Script to install dependencies using uv and run the example script
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Change to the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Changed to directory: $SCRIPT_DIR"
|
||||
|
||||
# Define variables
|
||||
VENV_DIR=".venv"
|
||||
REQUIREMENTS="sqlmodel pydantic"
|
||||
|
||||
# Check if uv is installed
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Error: uv is not installed."
|
||||
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies using uv..."
|
||||
uv pip install $REQUIREMENTS
|
||||
|
||||
# Make example.py executable
|
||||
chmod +x example.py
|
||||
|
||||
# Remove existing database file if it exists
|
||||
if [ -f "business.db" ]; then
|
||||
echo "Removing existing database file..."
|
||||
rm business.db
|
||||
fi
|
||||
|
||||
# Run the example script
|
||||
echo "Running example script..."
|
||||
python example.py
|
||||
|
||||
echo "Done!"
|
297
herodb/src/models/py/models.py
Normal file
297
herodb/src/models/py/models.py
Normal file
@ -0,0 +1,297 @@
|
||||
"""
|
||||
Python port of the business models from Rust using SQLModel.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
||||
class SaleStatus(str, Enum):
|
||||
"""SaleStatus represents the status of a sale"""
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class ProductType(str, Enum):
|
||||
"""ProductType represents the type of a product"""
|
||||
PRODUCT = "product"
|
||||
SERVICE = "service"
|
||||
|
||||
|
||||
class ProductStatus(str, Enum):
|
||||
"""ProductStatus represents the status of a product"""
|
||||
AVAILABLE = "available"
|
||||
UNAVAILABLE = "unavailable"
|
||||
|
||||
|
||||
class Currency(SQLModel, table=True):
|
||||
"""Currency represents a monetary value with amount and currency code"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
amount: float
|
||||
currency_code: str
|
||||
|
||||
@classmethod
|
||||
def new(cls, amount: float, currency_code: str) -> "Currency":
|
||||
"""Create a new currency with amount and code"""
|
||||
return cls(amount=amount, currency_code=currency_code)
|
||||
|
||||
|
||||
class Customer(SQLModel, table=True):
|
||||
"""Customer represents a customer who can purchase products or services"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
pubkey: str
|
||||
contact_sids_json: str = Field(default="[]")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
sales: List["Sale"] = Relationship(back_populates="customer")
|
||||
|
||||
@property
|
||||
def contact_sids(self) -> List[str]:
|
||||
"""Get the contact SIDs as a list"""
|
||||
return json.loads(self.contact_sids_json)
|
||||
|
||||
@contact_sids.setter
|
||||
def contact_sids(self, value: List[str]) -> None:
|
||||
"""Set the contact SIDs from a list"""
|
||||
self.contact_sids_json = json.dumps(value)
|
||||
|
||||
@classmethod
|
||||
def new(cls, name: str, description: str, pubkey: str, contact_sids: List[str] = None) -> "Customer":
|
||||
"""Create a new customer with default timestamps"""
|
||||
customer = cls(
|
||||
name=name,
|
||||
description=description,
|
||||
pubkey=pubkey,
|
||||
)
|
||||
if contact_sids:
|
||||
customer.contact_sids = contact_sids
|
||||
return customer
|
||||
|
||||
def add_contact(self, contact_id: int) -> None:
|
||||
"""Add a contact ID to the customer"""
|
||||
# In a real implementation, this would add a relationship to a Contact model
|
||||
# For simplicity, we're not implementing the Contact model in this example
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def add_contact_sid(self, circle_id: str, object_id: str) -> None:
|
||||
"""Add a smart ID (sid) to the customer's contact_sids list"""
|
||||
sid = f"{circle_id}_{object_id}"
|
||||
sids = self.contact_sids
|
||||
if sid not in sids:
|
||||
sids.append(sid)
|
||||
self.contact_sids = sids
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
|
||||
class ProductComponent(SQLModel, table=True):
|
||||
"""ProductComponent represents a component of a product"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
quantity: int
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||
product: Optional["Product"] = Relationship(back_populates="components")
|
||||
|
||||
@classmethod
|
||||
def new(cls, name: str, description: str, quantity: int) -> "ProductComponent":
|
||||
"""Create a new product component with default timestamps"""
|
||||
return cls(
|
||||
name=name,
|
||||
description=description,
|
||||
quantity=quantity,
|
||||
)
|
||||
|
||||
|
||||
class Product(SQLModel, table=True):
|
||||
"""Product represents a product or service offered"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
type_: ProductType = Field(sa_column_kwargs={"name": "type"})
|
||||
category: str
|
||||
status: ProductStatus
|
||||
max_amount: int
|
||||
purchase_till: datetime
|
||||
active_till: datetime
|
||||
istemplate: bool = Field(default=False)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Price relationship
|
||||
price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
price: Optional[Currency] = Relationship()
|
||||
|
||||
# Relationships
|
||||
components: List[ProductComponent] = Relationship(back_populates="product")
|
||||
sale_items: List["SaleItem"] = Relationship(back_populates="product")
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
name: str,
|
||||
description: str,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: str,
|
||||
status: ProductStatus,
|
||||
max_amount: int,
|
||||
validity_days: int,
|
||||
istemplate: bool = False,
|
||||
) -> "Product":
|
||||
"""Create a new product with default timestamps"""
|
||||
now = datetime.utcnow()
|
||||
return cls(
|
||||
name=name,
|
||||
description=description,
|
||||
price=price,
|
||||
type_=type_,
|
||||
category=category,
|
||||
status=status,
|
||||
max_amount=max_amount,
|
||||
purchase_till=now + timedelta(days=365),
|
||||
active_till=now + timedelta(days=validity_days),
|
||||
istemplate=istemplate,
|
||||
)
|
||||
|
||||
def add_component(self, component: ProductComponent) -> None:
|
||||
"""Add a component to this product"""
|
||||
component.product = self
|
||||
self.components.append(component)
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def set_purchase_period(self, purchase_till: datetime) -> None:
|
||||
"""Update the purchase availability timeframe"""
|
||||
self.purchase_till = purchase_till
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def set_active_period(self, active_till: datetime) -> None:
|
||||
"""Update the active timeframe"""
|
||||
self.active_till = active_till
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def is_purchasable(self) -> bool:
|
||||
"""Check if the product is available for purchase"""
|
||||
return self.status == ProductStatus.AVAILABLE and datetime.utcnow() <= self.purchase_till
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if the product is still active (for services)"""
|
||||
return datetime.utcnow() <= self.active_till
|
||||
|
||||
|
||||
class SaleItem(SQLModel, table=True):
|
||||
"""SaleItem represents an item in a sale"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
quantity: int
|
||||
active_till: datetime
|
||||
|
||||
# Relationships
|
||||
sale_id: Optional[int] = Field(default=None, foreign_key="sale.id")
|
||||
sale: Optional["Sale"] = Relationship(back_populates="items")
|
||||
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||
product: Optional[Product] = Relationship(back_populates="sale_items")
|
||||
|
||||
unit_price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
unit_price: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.unit_price_id]"})
|
||||
|
||||
subtotal_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
subtotal: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.subtotal_id]"})
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
product: Product,
|
||||
quantity: int,
|
||||
unit_price: Currency,
|
||||
active_till: datetime,
|
||||
) -> "SaleItem":
|
||||
"""Create a new sale item"""
|
||||
# Calculate subtotal
|
||||
amount = unit_price.amount * quantity
|
||||
subtotal = Currency(
|
||||
amount=amount,
|
||||
currency_code=unit_price.currency_code,
|
||||
)
|
||||
|
||||
return cls(
|
||||
name=product.name,
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
unit_price=unit_price,
|
||||
subtotal=subtotal,
|
||||
active_till=active_till,
|
||||
)
|
||||
|
||||
|
||||
class Sale(SQLModel, table=True):
|
||||
"""Sale represents a sale of products or services"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
status: SaleStatus
|
||||
sale_date: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
customer_id: Optional[int] = Field(default=None, foreign_key="customer.id")
|
||||
customer: Optional[Customer] = Relationship(back_populates="sales")
|
||||
|
||||
total_amount_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
total_amount: Optional[Currency] = Relationship()
|
||||
|
||||
items: List[SaleItem] = Relationship(back_populates="sale")
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
customer: Customer,
|
||||
currency_code: str,
|
||||
status: SaleStatus = SaleStatus.PENDING,
|
||||
) -> "Sale":
|
||||
"""Create a new sale with default timestamps"""
|
||||
total_amount = Currency(amount=0.0, currency_code=currency_code)
|
||||
|
||||
return cls(
|
||||
customer=customer,
|
||||
total_amount=total_amount,
|
||||
status=status,
|
||||
)
|
||||
|
||||
def add_item(self, item: SaleItem) -> None:
|
||||
"""Add an item to the sale and update the total amount"""
|
||||
item.sale = self
|
||||
|
||||
# Update the total amount
|
||||
if not self.items:
|
||||
# First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency(
|
||||
amount=item.subtotal.amount,
|
||||
currency_code=item.subtotal.currency_code,
|
||||
)
|
||||
else:
|
||||
# Add to the existing total
|
||||
# (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount
|
||||
|
||||
# Add the item to the list
|
||||
self.items.append(item)
|
||||
|
||||
# Update the sale timestamp
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def update_status(self, status: SaleStatus) -> None:
|
||||
"""Update the status of the sale"""
|
||||
self.status = status
|
||||
self.updated_at = datetime.utcnow()
|
42
herodb/src/models/py/server.sh
Executable file
42
herodb/src/models/py/server.sh
Executable file
@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Script to start the FastAPI server
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Change to the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Changed to directory: $SCRIPT_DIR"
|
||||
|
||||
# Define variables
|
||||
VENV_DIR=".venv"
|
||||
REQUIREMENTS="sqlmodel pydantic fastapi uvicorn"
|
||||
|
||||
# Check if uv is installed
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Error: uv is not installed."
|
||||
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies using uv..."
|
||||
uv pip install $REQUIREMENTS
|
||||
|
||||
# Make api.py executable
|
||||
chmod +x api.py
|
||||
|
||||
# Start the FastAPI server
|
||||
echo "Starting FastAPI server..."
|
||||
echo "API documentation available at: http://localhost:8000/docs"
|
||||
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
7
herodb/src/rhaiengine/mod.rs
Normal file
7
herodb/src/rhaiengine/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
//! Rhai Engine module for scripting support
|
||||
//!
|
||||
//! This module provides integration with the Rhai scripting language.
|
||||
|
||||
// Re-export the engine module
|
||||
pub mod engine;
|
||||
pub use engine::*;
|
Loading…
Reference in New Issue
Block a user