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 = "1.3.55"
|
||||||
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
|
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
rhai = "1.15.1"
|
rhai = "1.21.0"
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
@ -27,10 +27,22 @@ lazy_static = "1.4.0"
|
|||||||
name = "rhai_demo"
|
name = "rhai_demo"
|
||||||
path = "examples/rhai_demo.rs"
|
path = "examples/rhai_demo.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "business_models_demo"
|
||||||
|
path = "examples/business_models_demo.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "dbexample2"
|
name = "dbexample_prod"
|
||||||
path = "src/cmd/dbexample2/main.rs"
|
path = "src/cmd/dbexample_prod/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "dbexample_mcc"
|
name = "dbexample_mcc"
|
||||||
path = "src/cmd/dbexample_mcc/main.rs"
|
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
|
## example
|
||||||
|
|
||||||
```bash
|
```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
|
## 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 chrono::{Utc, Duration};
|
||||||
use herodb::db::DBBuilder;
|
use herodb::db::{DBBuilder, SledDB, SledModel};
|
||||||
use herodb::models::governance::{
|
use herodb::models::gov::{
|
||||||
Company, CompanyStatus, BusinessType,
|
Company, CompanyStatus, BusinessType,
|
||||||
Shareholder, ShareholderType,
|
Shareholder, ShareholderType,
|
||||||
Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus,
|
Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus,
|
||||||
@ -12,11 +12,11 @@ use std::path::PathBuf;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("DB Example: Governance Module");
|
println!("DB Example: Gov Module");
|
||||||
println!("============================");
|
println!("============================");
|
||||||
|
|
||||||
// Create a temporary directory for the database
|
// 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() {
|
if db_path.exists() {
|
||||||
fs::remove_dir_all(&db_path)?;
|
fs::remove_dir_all(&db_path)?;
|
||||||
}
|
}
|
||||||
@ -58,7 +58,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Insert the company
|
// Insert the company
|
||||||
db.insert(&company)?;
|
db.set(&company)?;
|
||||||
println!("Company created: {} (ID: {})", company.name, company.id);
|
println!("Company created: {} (ID: {})", company.name, company.id);
|
||||||
println!("Status: {:?}, Business Type: {}", company.status, company.business_type.as_str());
|
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
|
// Insert the users
|
||||||
db.insert(&user1)?;
|
db.set(&user1)?;
|
||||||
db.insert(&user2)?;
|
db.set(&user2)?;
|
||||||
db.insert(&user3)?;
|
db.set(&user3)?;
|
||||||
|
|
||||||
println!("User created: {} ({})", user1.name, user1.role);
|
println!("User created: {} ({})", user1.name, user1.role);
|
||||||
println!("User created: {} ({})", user2.name, user2.role);
|
println!("User created: {} ({})", user2.name, user2.role);
|
||||||
@ -137,9 +137,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Insert the shareholders
|
// Insert the shareholders
|
||||||
db.insert(&shareholder1)?;
|
db.set(&shareholder1)?;
|
||||||
db.insert(&shareholder2)?;
|
db.set(&shareholder2)?;
|
||||||
db.insert(&shareholder3)?;
|
db.set(&shareholder3)?;
|
||||||
|
|
||||||
println!("Shareholder created: {} ({} shares, {}%)",
|
println!("Shareholder created: {} ({} shares, {}%)",
|
||||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||||
@ -150,7 +150,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Update shareholder shares
|
// Update shareholder shares
|
||||||
shareholder1.update_shares(1100.0, 44.0);
|
shareholder1.update_shares(1100.0, 44.0);
|
||||||
db.insert(&shareholder1)?;
|
db.set(&shareholder1)?;
|
||||||
println!("Updated shareholder: {} ({} shares, {}%)",
|
println!("Updated shareholder: {} ({} shares, {}%)",
|
||||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
meeting.add_attendee(attendee3);
|
meeting.add_attendee(attendee3);
|
||||||
|
|
||||||
// Insert the meeting
|
// Insert the meeting
|
||||||
db.insert(&meeting)?;
|
db.set(&meeting)?;
|
||||||
println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M"));
|
println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M"));
|
||||||
println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len());
|
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) {
|
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) {
|
||||||
attendee.update_status(AttendeeStatus::Confirmed);
|
attendee.update_status(AttendeeStatus::Confirmed);
|
||||||
}
|
}
|
||||||
db.insert(&meeting)?;
|
db.set(&meeting)?;
|
||||||
|
|
||||||
// Get confirmed attendees
|
// Get confirmed attendees
|
||||||
let confirmed = meeting.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);
|
resolution.link_to_meeting(meeting.id);
|
||||||
|
|
||||||
// Insert the resolution
|
// Insert the resolution
|
||||||
db.insert(&resolution)?;
|
db.set(&resolution)?;
|
||||||
println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status);
|
println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status);
|
||||||
|
|
||||||
// Propose the resolution
|
// Propose the resolution
|
||||||
resolution.propose();
|
resolution.propose();
|
||||||
db.insert(&resolution)?;
|
db.set(&resolution)?;
|
||||||
println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d"));
|
println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d"));
|
||||||
|
|
||||||
// Add approvals
|
// Add approvals
|
||||||
resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string());
|
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(user2.id, user2.name.clone(), true, "Financials look good".to_string());
|
||||||
resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string());
|
resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string());
|
||||||
db.insert(&resolution)?;
|
db.set(&resolution)?;
|
||||||
|
|
||||||
// Check approval status
|
// Check approval status
|
||||||
println!("Approvals: {}, Rejections: {}",
|
println!("Approvals: {}, Rejections: {}",
|
||||||
@ -263,7 +263,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Approve the resolution
|
// Approve the resolution
|
||||||
resolution.approve();
|
resolution.approve();
|
||||||
db.insert(&resolution)?;
|
db.set(&resolution)?;
|
||||||
println!("Resolution approved on {}",
|
println!("Resolution approved on {}",
|
||||||
resolution.approved_at.unwrap().format("%Y-%m-%d"));
|
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);
|
vote.add_option("Abstain".to_string(), 0);
|
||||||
|
|
||||||
// Insert the vote
|
// Insert the vote
|
||||||
db.insert(&vote)?;
|
db.set(&vote)?;
|
||||||
println!("Vote created: {} (Status: {:?})", vote.title, vote.status);
|
println!("Vote created: {} (Status: {:?})", vote.title, vote.status);
|
||||||
println!("Voting period: {} to {}",
|
println!("Voting period: {} to {}",
|
||||||
vote.start_date.format("%Y-%m-%d"),
|
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(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(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
|
vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares
|
||||||
db.insert(&vote)?;
|
db.set(&vote)?;
|
||||||
|
|
||||||
// Check voting results
|
// Check voting results
|
||||||
println!("Voting results:");
|
println!("Voting results:");
|
||||||
@ -318,7 +318,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Link the resolution to the vote
|
// Link the resolution to the vote
|
||||||
vote_resolution.link_to_vote(vote.id);
|
vote_resolution.link_to_vote(vote.id);
|
||||||
vote_resolution.propose();
|
vote_resolution.propose();
|
||||||
db.insert(&vote_resolution)?;
|
db.set(&vote_resolution)?;
|
||||||
println!("Created resolution linked to vote: {}", vote_resolution.title);
|
println!("Created resolution linked to vote: {}", vote_resolution.title);
|
||||||
|
|
||||||
println!("\n7. Retrieving Related Objects");
|
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 bincode;
|
||||||
use brotli::{CompressorReader, Decompressor};
|
use brotli::{CompressorReader, Decompressor};
|
||||||
|
use rhai::CustomType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sled;
|
use sled;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
@ -41,12 +42,8 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
|||||||
const BROTLI_LGWIN: u32 = 22;
|
const BROTLI_LGWIN: u32 = 22;
|
||||||
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||||
|
|
||||||
let mut compressor = CompressorReader::new(
|
let mut compressor =
|
||||||
&encoded[..],
|
CompressorReader::new(&encoded[..], BUFFER_SIZE, BROTLI_QUALITY, BROTLI_LGWIN);
|
||||||
BUFFER_SIZE,
|
|
||||||
BROTLI_QUALITY,
|
|
||||||
BROTLI_LGWIN
|
|
||||||
);
|
|
||||||
compressor.read_to_end(&mut compressed)?;
|
compressor.read_to_end(&mut compressed)?;
|
||||||
|
|
||||||
Ok(compressed)
|
Ok(compressed)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use crate::db::base::*;
|
use crate::db::base::*;
|
||||||
|
use bincode;
|
||||||
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::fmt::Debug;
|
|
||||||
use bincode;
|
|
||||||
|
|
||||||
/// Represents a single database operation in a transaction
|
/// Represents a single database operation in a transaction
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -77,23 +78,25 @@ impl TransactionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Main DB manager that automatically handles all root models
|
/// Main DB manager that automatically handles all root models
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct DB {
|
pub struct DB {
|
||||||
db_path: PathBuf,
|
db_path: PathBuf,
|
||||||
|
|
||||||
// Type map for generic operations
|
// 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
|
// Locks to ensure thread safety for key areas
|
||||||
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
|
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
|
||||||
|
|
||||||
// Transaction state
|
// Transaction state
|
||||||
transaction: RwLock<Option<TransactionState>>,
|
transaction: Arc<RwLock<Option<TransactionState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for DB that allows registering models
|
/// Builder for DB that allows registering models
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct DBBuilder {
|
pub struct DBBuilder {
|
||||||
base_path: PathBuf,
|
base_path: PathBuf,
|
||||||
model_registrations: Vec<Box<dyn ModelRegistration>>,
|
model_registrations: Vec<Arc<dyn ModelRegistration>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait for model registration
|
/// Trait for model registration
|
||||||
@ -130,31 +133,43 @@ impl DBBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
/// Register a model type with the DB
|
||||||
pub fn register_model<T: SledModel>(mut self) -> Self {
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the DB with the registered models
|
/// 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;
|
let base_path = self.base_path;
|
||||||
|
|
||||||
// Ensure base directory exists
|
// Ensure base directory exists
|
||||||
if !base_path.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
|
// 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 {
|
for registration in self.model_registrations {
|
||||||
let (type_id, db) = registration.register(&base_path)?;
|
let (type_id, db) = registration.register(&base_path).map_err(|e| {
|
||||||
type_map.insert(type_id, db);
|
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 _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let transaction = RwLock::new(None);
|
let transaction = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
Ok(DB {
|
Ok(DB {
|
||||||
db_path: base_path,
|
db_path: base_path,
|
||||||
@ -176,7 +191,7 @@ impl DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||||
let transaction = RwLock::new(None);
|
let transaction = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db_path: base_path,
|
db_path: base_path,
|
||||||
@ -192,7 +207,9 @@ impl DB {
|
|||||||
pub fn begin_transaction(&self) -> SledDBResult<()> {
|
pub fn begin_transaction(&self) -> SledDBResult<()> {
|
||||||
let mut tx = self.transaction.write().unwrap();
|
let mut tx = self.transaction.write().unwrap();
|
||||||
if tx.is_some() {
|
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());
|
*tx = Some(TransactionState::new());
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -212,7 +229,10 @@ impl DB {
|
|||||||
return db_ops.insert_any_raw(serialized);
|
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
|
/// Commit the current transaction, applying all operations
|
||||||
@ -227,11 +247,16 @@ impl DB {
|
|||||||
// Execute all operations in the transaction
|
// Execute all operations in the transaction
|
||||||
for op in tx_state.operations {
|
for op in tx_state.operations {
|
||||||
match op {
|
match op {
|
||||||
DbOperation::Set { model_type, serialized } => {
|
DbOperation::Set {
|
||||||
|
model_type,
|
||||||
|
serialized,
|
||||||
|
} => {
|
||||||
self.apply_set_operation(model_type, &serialized)?;
|
self.apply_set_operation(model_type, &serialized)?;
|
||||||
},
|
}
|
||||||
DbOperation::Delete { model_type, id } => {
|
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)?;
|
.ok_or_else(|| SledDBError::TypeError)?;
|
||||||
db_ops.delete(&id)?;
|
db_ops.delete(&id)?;
|
||||||
}
|
}
|
||||||
@ -310,14 +335,20 @@ impl DB {
|
|||||||
for op in tx_state.operations.iter().rev() {
|
for op in tx_state.operations.iter().rev() {
|
||||||
match op {
|
match op {
|
||||||
// First check if this ID has been deleted in the transaction
|
// 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 {
|
if *model_type == type_id && op_id == id {
|
||||||
// Return NotFound error for deleted records
|
// Return NotFound error for deleted records
|
||||||
return Some(Err(SledDBError::NotFound(id.to_string())));
|
return Some(Err(SledDBError::NotFound(id.to_string())));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// Then check if it has been set in the transaction
|
// 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 {
|
if *model_type == type_id {
|
||||||
// Try to deserialize and check the ID
|
// Try to deserialize and check the ID
|
||||||
match bincode::deserialize::<T>(serialized) {
|
match bincode::deserialize::<T>(serialized) {
|
||||||
@ -325,7 +356,7 @@ impl DB {
|
|||||||
if model.get_id() == id_str {
|
if model.get_id() == id_str {
|
||||||
return Some(Ok(Some(model)));
|
return Some(Ok(Some(model)));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(_) => continue, // Skip if deserialization fails
|
Err(_) => continue, // Skip if deserialization fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -358,7 +389,7 @@ impl DB {
|
|||||||
Ok(t) => Ok(*t),
|
Ok(t) => Ok(*t),
|
||||||
Err(_) => Err(SledDBError::TypeError),
|
Err(_) => Err(SledDBError::TypeError),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => Err(SledDBError::TypeError),
|
None => Err(SledDBError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,7 +434,7 @@ impl DB {
|
|||||||
Ok(vec_t) => Ok(*vec_t),
|
Ok(vec_t) => Ok(*vec_t),
|
||||||
Err(_) => Err(SledDBError::TypeError),
|
Err(_) => Err(SledDBError::TypeError),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => Err(SledDBError::TypeError),
|
None => Err(SledDBError::TypeError),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -412,7 +443,7 @@ impl DB {
|
|||||||
pub fn register<T: SledModel>(&mut self) -> SledDBResult<()> {
|
pub fn register<T: SledModel>(&mut self) -> SledDBResult<()> {
|
||||||
let db_path = self.db_path.join(T::db_prefix());
|
let db_path = self.db_path.join(T::db_prefix());
|
||||||
let db: SledDB<T> = SledDB::open(db_path)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -420,7 +451,10 @@ impl DB {
|
|||||||
pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
|
pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
|
||||||
match self.type_map.get(&TypeId::of::<T>()) {
|
match self.type_map.get(&TypeId::of::<T>()) {
|
||||||
Some(db) => Ok(&**db),
|
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,22 +5,24 @@ macro_rules! impl_model_methods {
|
|||||||
impl DB {
|
impl DB {
|
||||||
paste::paste! {
|
paste::paste! {
|
||||||
/// Insert a model instance into the database
|
/// Insert a model instance into the database
|
||||||
pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
|
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
self.set(item)
|
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
|
/// 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())
|
self.get::<$model>(&id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a model instance by its ID
|
/// 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())
|
self.delete::<$model>(&id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all model instances
|
/// List all model instances
|
||||||
pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
|
pub fn [<list_ $plural>](&mut self) -> SledDBResult<Vec<$model>> {
|
||||||
self.list::<$model>()
|
self.list::<$model>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
|
// Temporarily commented out due to compilation errors
|
||||||
|
// pub mod rhaiengine;
|
||||||
|
pub mod cmd;
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization
|
|||||||
## Model Relationships
|
## Model Relationships
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐
|
||||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │
|
│ Customer │
|
||||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
||||||
▲ │
|
▲ │
|
||||||
│ │
|
│ │
|
||||||
┌─────┴──────────┐ │
|
┌─────┴──────────┐ │
|
||||||
│ProductComponent│ │
|
│ProductComponent│ │
|
||||||
└────────────────┘ │
|
└────────────────┘ │
|
||||||
|
│
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────┐
|
┌─────────────┐ ┌─────────────┐
|
||||||
│ Sale │
|
│ 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
|
||||||
|
|
||||||
- root objects are the one who are stored in the DB
|
- Root objects are the ones stored directly in the DB
|
||||||
- Root Objects are
|
- Root Objects are:
|
||||||
- currency
|
- Customer
|
||||||
- product
|
- Currency
|
||||||
|
- Product
|
||||||
- Sale
|
- Sale
|
||||||
|
- Service
|
||||||
|
- Invoice
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code.
|
|||||||
**Builder:**
|
**Builder:**
|
||||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
- `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
|
### Product
|
||||||
|
|
||||||
#### ProductType Enum
|
#### ProductType Enum
|
||||||
@ -74,7 +117,7 @@ Represents a component part of a product.
|
|||||||
Represents a product or service offered.
|
Represents a product or service offered.
|
||||||
|
|
||||||
**Properties:**
|
**Properties:**
|
||||||
- `id`: u32 - Unique identifier
|
- `id`: i64 - Unique identifier
|
||||||
- `name`: String - Product name
|
- `name`: String - Product name
|
||||||
- `description`: String - Product description
|
- `description`: String - Product description
|
||||||
- `price`: Currency - Product price
|
- `price`: Currency - Product price
|
||||||
@ -83,10 +126,11 @@ Represents a product or service offered.
|
|||||||
- `status`: ProductStatus - Available or Unavailable
|
- `status`: ProductStatus - Available or Unavailable
|
||||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||||
- `updated_at`: DateTime<Utc> - Last update 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
|
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||||
- `active_till`: DateTime<Utc> - When product/service expires
|
- `active_till`: DateTime<Utc> - When product/service expires
|
||||||
- `components`: Vec<ProductComponent> - List of product components
|
- `components`: Vec<ProductComponent> - List of product components
|
||||||
|
- `is_template`: bool - Whether this is a template product (to be added)
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
- `add_component()` - Adds a component to this product
|
- `add_component()` - Adds a component to this product
|
||||||
@ -104,6 +148,61 @@ Represents a product or service offered.
|
|||||||
- `get_id()` - Returns the ID as a string
|
- `get_id()` - Returns the ID as a string
|
||||||
- `db_prefix()` - Returns "product" as the database prefix
|
- `db_prefix()` - Returns "product" as the database prefix
|
||||||
|
|
||||||
|
### 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
|
### Sale
|
||||||
|
|
||||||
#### SaleStatus Enum
|
#### SaleStatus Enum
|
||||||
@ -120,11 +219,18 @@ Represents an item within a sale.
|
|||||||
- `sale_id`: u32 - Parent sale ID
|
- `sale_id`: u32 - Parent sale ID
|
||||||
- `product_id`: u32 - ID of the product sold
|
- `product_id`: u32 - ID of the product sold
|
||||||
- `name`: String - Product name at time of sale
|
- `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
|
- `quantity`: i32 - Number of items purchased
|
||||||
- `unit_price`: Currency - Price per unit
|
- `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
|
- `active_till`: DateTime<Utc> - When item/service expires
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- `total_with_tax()` - Returns the total amount including tax
|
||||||
|
|
||||||
**Builder:**
|
**Builder:**
|
||||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||||
|
|
||||||
@ -134,18 +240,24 @@ Represents a complete sale transaction.
|
|||||||
**Properties:**
|
**Properties:**
|
||||||
- `id`: u32 - Unique identifier
|
- `id`: u32 - Unique identifier
|
||||||
- `company_id`: u32 - ID of the company making the sale
|
- `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_name`: String - Name of the buyer
|
||||||
- `buyer_email`: String - Email 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
|
- `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
|
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||||
- `items`: Vec<SaleItem> - List of items in the sale
|
- `items`: Vec<SaleItem> - List of items in the sale
|
||||||
|
|
||||||
**Methods:**
|
**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
|
- `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:**
|
**Builder:**
|
||||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||||
@ -223,6 +335,7 @@ let item = SaleItemBuilder::new()
|
|||||||
.name("Premium Service")
|
.name("Premium Service")
|
||||||
.quantity(1)
|
.quantity(1)
|
||||||
.unit_price(unit_price)
|
.unit_price(unit_price)
|
||||||
|
.tax_rate(20.0) // 20% tax rate
|
||||||
.active_till(now + Duration::days(30))
|
.active_till(now + Duration::days(30))
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build sale item");
|
.expect("Failed to build sale item");
|
||||||
@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new()
|
|||||||
|
|
||||||
// Update the sale status
|
// Update the sale status
|
||||||
sale.update_status(SaleStatus::Completed);
|
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
|
## 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_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
- `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 crate::db::base::{SledModel, Storable};
|
||||||
use serde::{Deserialize, Serialize};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
|
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||||
|
|
||||||
/// Currency represents a monetary value with amount and currency code
|
/// 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 struct Currency {
|
||||||
pub amount: f64,
|
pub amount: f64,
|
||||||
pub currency_code: String,
|
pub currency_code: String,
|
||||||
@ -19,28 +19,13 @@ impl Currency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the currency to USD
|
pub fn amount(&mut self) -> f64 {
|
||||||
pub fn to_usd(&self) -> Option<Currency> {
|
self.amount
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for Currency
|
/// Builder for Currency
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct CurrencyBuilder {
|
pub struct CurrencyBuilder {
|
||||||
amount: Option<f64>,
|
amount: Option<f64>,
|
||||||
currency_code: Option<String>,
|
currency_code: Option<String>,
|
||||||
@ -68,7 +53,7 @@ impl CurrencyBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the Currency object
|
/// Build the Currency object
|
||||||
pub fn build(self) -> Result<Currency, &'static str> {
|
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
|
||||||
Ok(Currency {
|
Ok(Currency {
|
||||||
amount: self.amount.ok_or("amount is required")?,
|
amount: self.amount.ok_or("amount is required")?,
|
||||||
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
||||||
|
@ -27,15 +27,17 @@ pub struct Payment {
|
|||||||
pub amount: Currency,
|
pub amount: Currency,
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
||||||
pub method: String,
|
pub method: String,
|
||||||
|
pub comment: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Payment {
|
impl Payment {
|
||||||
/// Create a new payment
|
/// Create a new payment
|
||||||
pub fn new(amount: Currency, method: String) -> Self {
|
pub fn new(amount: Currency, method: String, comment: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
amount,
|
amount,
|
||||||
date: Utc::now(),
|
date: Utc::now(),
|
||||||
method,
|
method,
|
||||||
|
comment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc, Duration};
|
use crate::db::base::{SledModel, Storable};
|
||||||
use serde::{Deserialize, Serialize};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||||
use crate::models::biz::exchange_rate::EXCHANGE_RATE_SERVICE;
|
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||||
|
|
||||||
/// ProductType represents the type of a product
|
/// ProductType represents the type of a product
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@ -13,10 +13,6 @@ pub enum ProductType {
|
|||||||
/// ProductStatus represents the status of a product
|
/// ProductStatus represents the status of a product
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum ProductStatus {
|
pub enum ProductStatus {
|
||||||
Active,
|
|
||||||
Error,
|
|
||||||
EndOfLife,
|
|
||||||
Paused,
|
|
||||||
Available,
|
Available,
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
@ -24,19 +20,17 @@ pub enum ProductStatus {
|
|||||||
/// ProductComponent represents a component of a product
|
/// ProductComponent represents a component of a product
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProductComponent {
|
pub struct ProductComponent {
|
||||||
pub id: u32,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub quantity: i32,
|
pub quantity: i64,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_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 {
|
impl ProductComponent {
|
||||||
/// Create a new product component with default timestamps
|
/// 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();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@ -45,32 +39,19 @@ impl ProductComponent {
|
|||||||
quantity,
|
quantity,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_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
|
/// Builder for ProductComponent
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct ProductComponentBuilder {
|
pub struct ProductComponentBuilder {
|
||||||
id: Option<u32>,
|
id: Option<i64>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
quantity: Option<i32>,
|
quantity: Option<i64>,
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
updated_at: Option<DateTime<Utc>>,
|
updated_at: Option<DateTime<Utc>>,
|
||||||
energy_usage: Option<f64>,
|
|
||||||
cost: Option<Currency>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProductComponentBuilder {
|
impl ProductComponentBuilder {
|
||||||
@ -83,13 +64,11 @@ impl ProductComponentBuilder {
|
|||||||
quantity: None,
|
quantity: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
energy_usage: None,
|
|
||||||
cost: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the id
|
/// Set the id
|
||||||
pub fn id(mut self, id: u32) -> Self {
|
pub fn id(mut self, id: i64) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -107,7 +86,7 @@ impl ProductComponentBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the quantity
|
/// Set the quantity
|
||||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
pub fn quantity(mut self, quantity: i64) -> Self {
|
||||||
self.quantity = Some(quantity);
|
self.quantity = Some(quantity);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -124,20 +103,8 @@ impl ProductComponentBuilder {
|
|||||||
self
|
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
|
/// Build the ProductComponent object
|
||||||
pub fn build(self) -> Result<ProductComponent, &'static str> {
|
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Ok(ProductComponent {
|
Ok(ProductComponent {
|
||||||
id: self.id.ok_or("id is required")?,
|
id: self.id.ok_or("id is required")?,
|
||||||
@ -146,16 +113,19 @@ impl ProductComponentBuilder {
|
|||||||
quantity: self.quantity.ok_or("quantity is required")?,
|
quantity: self.quantity.ok_or("quantity is required")?,
|
||||||
created_at: self.created_at.unwrap_or(now),
|
created_at: self.created_at.unwrap_or(now),
|
||||||
updated_at: self.updated_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
|
/// Product represents a product or service offered in the system
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[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 struct Product {
|
||||||
pub id: u32,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub price: Currency,
|
pub price: Currency,
|
||||||
@ -164,7 +134,7 @@ pub struct Product {
|
|||||||
pub status: ProductStatus,
|
pub status: ProductStatus,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_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 purchase_till: DateTime<Utc>,
|
||||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||||
pub components: Vec<ProductComponent>,
|
pub components: Vec<ProductComponent>,
|
||||||
@ -175,14 +145,14 @@ pub struct Product {
|
|||||||
impl Product {
|
impl Product {
|
||||||
/// Create a new product with default timestamps
|
/// Create a new product with default timestamps
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: u32,
|
id: i64,
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
price: Currency,
|
price: Currency,
|
||||||
type_: ProductType,
|
type_: ProductType,
|
||||||
category: String,
|
category: String,
|
||||||
status: ProductStatus,
|
status: ProductStatus,
|
||||||
max_amount: u16,
|
max_amount: i64,
|
||||||
validity_days: i64, // How many days the product is valid after purchase
|
validity_days: i64, // How many days the product is valid after purchase
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
@ -224,65 +194,19 @@ impl Product {
|
|||||||
|
|
||||||
/// Check if the product is available for purchase
|
/// Check if the product is available for purchase
|
||||||
pub fn is_purchasable(&self) -> bool {
|
pub fn is_purchasable(&self) -> bool {
|
||||||
(self.status == ProductStatus::Available || self.status == ProductStatus::Active)
|
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||||
&& Utc::now() <= self.purchase_till
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the product is still active (for services)
|
/// Check if the product is still active (for services)
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
Utc::now() <= self.active_till
|
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
|
/// Builder for Product
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct ProductBuilder {
|
pub struct ProductBuilder {
|
||||||
id: Option<u32>,
|
id: Option<i64>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
price: Option<Currency>,
|
price: Option<Currency>,
|
||||||
@ -291,7 +215,7 @@ pub struct ProductBuilder {
|
|||||||
status: Option<ProductStatus>,
|
status: Option<ProductStatus>,
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
updated_at: Option<DateTime<Utc>>,
|
updated_at: Option<DateTime<Utc>>,
|
||||||
max_amount: Option<u16>,
|
max_amount: Option<i64>,
|
||||||
purchase_till: Option<DateTime<Utc>>,
|
purchase_till: Option<DateTime<Utc>>,
|
||||||
active_till: Option<DateTime<Utc>>,
|
active_till: Option<DateTime<Utc>>,
|
||||||
components: Vec<ProductComponent>,
|
components: Vec<ProductComponent>,
|
||||||
@ -320,7 +244,7 @@ impl ProductBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the id
|
/// Set the id
|
||||||
pub fn id(mut self, id: u32) -> Self {
|
pub fn id(mut self, id: i64) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -362,7 +286,7 @@ impl ProductBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the max amount
|
/// 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.max_amount = Some(max_amount);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -400,9 +324,11 @@ impl ProductBuilder {
|
|||||||
// Calculate purchase_till and active_till based on validity_days if not set directly
|
// 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 purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
|
||||||
let active_till = if let Some(validity_days) = self.validity_days {
|
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 {
|
} 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 {
|
Ok(Product {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
use crate::db::base::{SledModel, Storable};
|
||||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
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 super::db::Model; // Removed old Model trait import
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use rhai::{CustomType, TypeBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
// use std::collections::HashMap; // Removed unused import
|
// use std::collections::HashMap; // Removed unused import
|
||||||
|
|
||||||
@ -20,9 +21,13 @@ pub struct SaleItem {
|
|||||||
pub sale_id: u32,
|
pub sale_id: u32,
|
||||||
pub product_id: u32,
|
pub product_id: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String, // Description of the item
|
||||||
|
pub comments: String, // Additional comments about the item
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub unit_price: Currency,
|
pub unit_price: Currency,
|
||||||
pub subtotal: 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
|
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,
|
sale_id: u32,
|
||||||
product_id: u32,
|
product_id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
description: String,
|
||||||
|
comments: String,
|
||||||
quantity: i32,
|
quantity: i32,
|
||||||
unit_price: Currency,
|
unit_price: Currency,
|
||||||
|
tax_rate: f64,
|
||||||
active_till: DateTime<Utc>,
|
active_till: DateTime<Utc>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Calculate subtotal
|
// Calculate subtotal (before tax)
|
||||||
let amount = unit_price.amount * quantity as f64;
|
let amount = unit_price.amount * quantity as f64;
|
||||||
let subtotal = Currency {
|
let subtotal = Currency {
|
||||||
amount,
|
amount,
|
||||||
currency_code: unit_price.currency_code.clone(),
|
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 {
|
Self {
|
||||||
id,
|
id,
|
||||||
sale_id,
|
sale_id,
|
||||||
product_id,
|
product_id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
|
comments,
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
active_till,
|
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
|
/// Builder for SaleItem
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct SaleItemBuilder {
|
pub struct SaleItemBuilder {
|
||||||
id: Option<u32>,
|
id: Option<u32>,
|
||||||
sale_id: Option<u32>,
|
sale_id: Option<u32>,
|
||||||
product_id: Option<u32>,
|
product_id: Option<u32>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
comments: Option<String>,
|
||||||
quantity: Option<i32>,
|
quantity: Option<i32>,
|
||||||
unit_price: Option<Currency>,
|
unit_price: Option<Currency>,
|
||||||
subtotal: Option<Currency>,
|
subtotal: Option<Currency>,
|
||||||
|
tax_rate: Option<f64>,
|
||||||
|
tax_amount: Option<Currency>,
|
||||||
active_till: Option<DateTime<Utc>>,
|
active_till: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,9 +109,13 @@ impl SaleItemBuilder {
|
|||||||
sale_id: None,
|
sale_id: None,
|
||||||
product_id: None,
|
product_id: None,
|
||||||
name: None,
|
name: None,
|
||||||
|
description: None,
|
||||||
|
comments: None,
|
||||||
quantity: None,
|
quantity: None,
|
||||||
unit_price: None,
|
unit_price: None,
|
||||||
subtotal: None,
|
subtotal: None,
|
||||||
|
tax_rate: None,
|
||||||
|
tax_amount: None,
|
||||||
active_till: None,
|
active_till: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,6 +144,18 @@ impl SaleItemBuilder {
|
|||||||
self
|
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
|
/// Set the quantity
|
||||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||||
self.quantity = Some(quantity);
|
self.quantity = Some(quantity);
|
||||||
@ -120,6 +168,12 @@ impl SaleItemBuilder {
|
|||||||
self
|
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
|
/// Set the active_till
|
||||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||||
self.active_till = Some(active_till);
|
self.active_till = Some(active_till);
|
||||||
@ -130,6 +184,7 @@ impl SaleItemBuilder {
|
|||||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||||
let quantity = self.quantity.ok_or("quantity 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
|
// Calculate subtotal
|
||||||
let amount = unit_price.amount * quantity as f64;
|
let amount = unit_price.amount * quantity as f64;
|
||||||
@ -138,28 +193,43 @@ impl SaleItemBuilder {
|
|||||||
currency_code: unit_price.currency_code.clone(),
|
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 {
|
Ok(SaleItem {
|
||||||
id: self.id.ok_or("id is required")?,
|
id: self.id.ok_or("id is required")?,
|
||||||
sale_id: self.sale_id.ok_or("sale_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")?,
|
product_id: self.product_id.ok_or("product_id is required")?,
|
||||||
name: self.name.ok_or("name is required")?,
|
name: self.name.ok_or("name is required")?,
|
||||||
|
description: self.description.unwrap_or_default(),
|
||||||
|
comments: self.comments.unwrap_or_default(),
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
active_till: self.active_till.ok_or("active_till is required")?,
|
active_till: self.active_till.ok_or("active_till is required")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sale represents a sale of products or services
|
/// Sale represents a sale of products or services
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||||
pub struct Sale {
|
pub struct Sale {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub company_id: u32,
|
pub company_id: u32,
|
||||||
|
pub customer_id: u32, // ID of the customer making the purchase
|
||||||
pub buyer_name: String,
|
pub buyer_name: String,
|
||||||
pub buyer_email: 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 status: SaleStatus,
|
||||||
|
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||||
pub sale_date: DateTime<Utc>,
|
pub sale_date: DateTime<Utc>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
@ -173,19 +243,29 @@ impl Sale {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
id: u32,
|
id: u32,
|
||||||
company_id: u32,
|
company_id: u32,
|
||||||
|
customer_id: u32,
|
||||||
buyer_name: String,
|
buyer_name: String,
|
||||||
buyer_email: String,
|
buyer_email: String,
|
||||||
currency_code: String,
|
currency_code: String,
|
||||||
status: SaleStatus,
|
status: SaleStatus,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
let zero_currency = Currency {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: currency_code.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
company_id,
|
company_id,
|
||||||
|
customer_id,
|
||||||
buyer_name,
|
buyer_name,
|
||||||
buyer_email,
|
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,
|
status,
|
||||||
|
service_id: None,
|
||||||
sale_date: now,
|
sale_date: now,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@ -198,17 +278,27 @@ impl Sale {
|
|||||||
// Make sure the item's sale_id matches this sale
|
// Make sure the item's sale_id matches this sale
|
||||||
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
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() {
|
if self.items.is_empty() {
|
||||||
// First item, initialize the total amount with the same currency
|
// First item, initialize the amounts with the same currency
|
||||||
self.total_amount = Currency {
|
self.subtotal_amount = Currency {
|
||||||
amount: item.subtotal.amount,
|
amount: item.subtotal.amount,
|
||||||
currency_code: item.subtotal.currency_code.clone(),
|
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 {
|
} else {
|
||||||
// Add to the existing total
|
// Add to the existing totals
|
||||||
// (Assumes all items have the same currency)
|
// (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
|
// Add the item to the list
|
||||||
@ -218,21 +308,107 @@ impl Sale {
|
|||||||
self.updated_at = Utc::now();
|
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
|
/// Update the status of the sale
|
||||||
pub fn update_status(&mut self, status: SaleStatus) {
|
pub fn update_status(&mut self, status: SaleStatus) {
|
||||||
self.status = status;
|
self.status = status;
|
||||||
self.updated_at = Utc::now();
|
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
|
/// Builder for Sale
|
||||||
|
#[derive(Clone, CustomType)]
|
||||||
pub struct SaleBuilder {
|
pub struct SaleBuilder {
|
||||||
id: Option<u32>,
|
id: Option<u32>,
|
||||||
company_id: Option<u32>,
|
company_id: Option<u32>,
|
||||||
|
customer_id: Option<u32>,
|
||||||
buyer_name: Option<String>,
|
buyer_name: Option<String>,
|
||||||
buyer_email: Option<String>,
|
buyer_email: Option<String>,
|
||||||
|
subtotal_amount: Option<Currency>,
|
||||||
|
tax_amount: Option<Currency>,
|
||||||
total_amount: Option<Currency>,
|
total_amount: Option<Currency>,
|
||||||
status: Option<SaleStatus>,
|
status: Option<SaleStatus>,
|
||||||
|
service_id: Option<u32>,
|
||||||
sale_date: Option<DateTime<Utc>>,
|
sale_date: Option<DateTime<Utc>>,
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
updated_at: Option<DateTime<Utc>>,
|
updated_at: Option<DateTime<Utc>>,
|
||||||
@ -246,10 +422,14 @@ impl SaleBuilder {
|
|||||||
Self {
|
Self {
|
||||||
id: None,
|
id: None,
|
||||||
company_id: None,
|
company_id: None,
|
||||||
|
customer_id: None,
|
||||||
buyer_name: None,
|
buyer_name: None,
|
||||||
buyer_email: None,
|
buyer_email: None,
|
||||||
|
subtotal_amount: None,
|
||||||
|
tax_amount: None,
|
||||||
total_amount: None,
|
total_amount: None,
|
||||||
status: None,
|
status: None,
|
||||||
|
service_id: None,
|
||||||
sale_date: None,
|
sale_date: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
@ -270,6 +450,12 @@ impl SaleBuilder {
|
|||||||
self
|
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
|
/// Set the buyer_name
|
||||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||||
self.buyer_name = Some(buyer_name.into());
|
self.buyer_name = Some(buyer_name.into());
|
||||||
@ -294,6 +480,12 @@ impl SaleBuilder {
|
|||||||
self
|
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
|
/// Set the sale_date
|
||||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||||
self.sale_date = Some(sale_date);
|
self.sale_date = Some(sale_date);
|
||||||
@ -312,39 +504,45 @@ impl SaleBuilder {
|
|||||||
let id = self.id.ok_or("id is required")?;
|
let id = self.id.ok_or("id is required")?;
|
||||||
let currency_code = self.currency_code.ok_or("currency_code 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 {
|
let mut total_amount = Currency {
|
||||||
amount: 0.0,
|
amount: 0.0,
|
||||||
currency_code: currency_code.clone(),
|
currency_code: currency_code.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total amount from items
|
// Calculate amounts from items
|
||||||
for item in &self.items {
|
for item in &self.items {
|
||||||
// Make sure the item's sale_id matches this sale
|
// Make sure the item's sale_id matches this sale
|
||||||
if item.sale_id != id {
|
if item.sale_id != id {
|
||||||
return Err("Item sale_id must match sale id");
|
return Err("Item sale_id must match sale id");
|
||||||
}
|
}
|
||||||
|
|
||||||
if total_amount.amount == 0.0 {
|
subtotal_amount.amount += item.subtotal.amount;
|
||||||
// First item, initialize the total amount with the same currency
|
tax_amount.amount += item.tax_amount.amount;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total amount
|
||||||
|
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||||
|
|
||||||
Ok(Sale {
|
Ok(Sale {
|
||||||
id,
|
id,
|
||||||
company_id: self.company_id.ok_or("company_id is required")?,
|
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_name: self.buyer_name.ok_or("buyer_name is required")?,
|
||||||
buyer_email: self.buyer_email.ok_or("buyer_email 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),
|
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||||
status: self.status.ok_or("status is required")?,
|
status: self.status.ok_or("status is required")?,
|
||||||
|
service_id: self.service_id,
|
||||||
sale_date: self.sale_date.unwrap_or(now),
|
sale_date: self.sale_date.unwrap_or(now),
|
||||||
created_at: self.created_at.unwrap_or(now),
|
created_at: self.created_at.unwrap_or(now),
|
||||||
updated_at: self.updated_at.unwrap_or(now),
|
updated_at: self.updated_at.unwrap_or(now),
|
||||||
|
@ -29,6 +29,8 @@ pub struct ServiceItem {
|
|||||||
pub service_id: u32,
|
pub service_id: u32,
|
||||||
pub product_id: u32,
|
pub product_id: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String, // Description of the service item
|
||||||
|
pub comments: String, // Additional comments about the service item
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub unit_price: Currency,
|
pub unit_price: Currency,
|
||||||
pub subtotal: Currency,
|
pub subtotal: Currency,
|
||||||
@ -45,6 +47,8 @@ impl ServiceItem {
|
|||||||
service_id: u32,
|
service_id: u32,
|
||||||
product_id: u32,
|
product_id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
description: String,
|
||||||
|
comments: String,
|
||||||
quantity: i32,
|
quantity: i32,
|
||||||
unit_price: Currency,
|
unit_price: Currency,
|
||||||
tax_rate: f64,
|
tax_rate: f64,
|
||||||
@ -76,6 +80,8 @@ impl ServiceItem {
|
|||||||
service_id,
|
service_id,
|
||||||
product_id,
|
product_id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
|
comments,
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
@ -117,6 +123,8 @@ pub struct ServiceItemBuilder {
|
|||||||
service_id: Option<u32>,
|
service_id: Option<u32>,
|
||||||
product_id: Option<u32>,
|
product_id: Option<u32>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
comments: Option<String>,
|
||||||
quantity: Option<i32>,
|
quantity: Option<i32>,
|
||||||
unit_price: Option<Currency>,
|
unit_price: Option<Currency>,
|
||||||
subtotal: Option<Currency>,
|
subtotal: Option<Currency>,
|
||||||
@ -134,6 +142,8 @@ impl ServiceItemBuilder {
|
|||||||
service_id: None,
|
service_id: None,
|
||||||
product_id: None,
|
product_id: None,
|
||||||
name: None,
|
name: None,
|
||||||
|
description: None,
|
||||||
|
comments: None,
|
||||||
quantity: None,
|
quantity: None,
|
||||||
unit_price: None,
|
unit_price: None,
|
||||||
subtotal: None,
|
subtotal: None,
|
||||||
@ -168,6 +178,18 @@ impl ServiceItemBuilder {
|
|||||||
self
|
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
|
/// Set the quantity
|
||||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||||
self.quantity = Some(quantity);
|
self.quantity = Some(quantity);
|
||||||
@ -230,6 +252,8 @@ impl ServiceItemBuilder {
|
|||||||
service_id: self.service_id.ok_or("service_id is required")?,
|
service_id: self.service_id.ok_or("service_id is required")?,
|
||||||
product_id: self.product_id.ok_or("product_id is required")?,
|
product_id: self.product_id.ok_or("product_id is required")?,
|
||||||
name: self.name.ok_or("name is required")?,
|
name: self.name.ok_or("name is required")?,
|
||||||
|
description: self.description.unwrap_or_default(),
|
||||||
|
comments: self.comments.unwrap_or_default(),
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
@ -2,33 +2,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::db::{SledModel, Storable};
|
use crate::db::{SledModel, Storable};
|
||||||
use std::collections::HashMap;
|
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)
|
/// Circle represents a collection of members (users or other circles)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Circle {
|
pub struct Circle {
|
||||||
pub id: u32, // unique id
|
pub id: u32, // unique id
|
||||||
pub name: String, // name of the circle
|
pub name: String, // name of the circle
|
||||||
pub description: String, // optional description
|
pub description: String, // optional description
|
||||||
pub members: Vec<Member>, // members of the circle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Circle {
|
impl Circle {
|
||||||
@ -38,15 +17,9 @@ impl Circle {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
description,
|
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
|
/// Returns a map of index keys for this circle
|
||||||
pub fn index_keys(&self) -> HashMap<String, String> {
|
pub fn index_keys(&self) -> HashMap<String, String> {
|
||||||
let mut keys = HashMap::new();
|
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 circle;
|
||||||
|
pub mod member;
|
||||||
pub mod name;
|
pub mod name;
|
||||||
|
pub mod wallet;
|
||||||
|
|
||||||
// Re-export all model types for convenience
|
// 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 name::{Name, Record, RecordType};
|
||||||
|
pub use wallet::{Wallet, Asset};
|
||||||
|
|
||||||
// Re-export database components from db module
|
// Re-export database components from db module
|
||||||
pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};
|
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 chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
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
|
/// CommitteeRole represents the role of a member in a committee
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
@ -167,8 +167,8 @@ impl Company {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get all resolutions for this 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> {
|
pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
|
||||||
let all_resolutions = db.list()?;
|
let all_resolutions = db.list::<super::Resolution>()?;
|
||||||
let company_resolutions = all_resolutions
|
let company_resolutions = all_resolutions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|resolution| resolution.company_id == self.id)
|
.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
|
/// 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> {
|
pub fn get_resolutions(&self, db: &crate::db::DB) -> Result<Vec<super::Resolution>, SledDBError> {
|
||||||
let all_resolutions = db.list()?;
|
let all_resolutions = db.list::<super::Resolution>()?;
|
||||||
let meeting_resolutions = all_resolutions
|
let meeting_resolutions = all_resolutions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|resolution| resolution.meeting_id == Some(self.id))
|
.filter(|resolution| resolution.meeting_id == Some(self.id))
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::db::{SledModel, Storable, SledDB, SledDBError};
|
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
|
/// ResolutionStatus represents the status of a resolution
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@ -158,10 +158,10 @@ impl Resolution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the meeting associated with this 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 {
|
match self.meeting_id {
|
||||||
Some(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))
|
Ok(Some(meeting))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
@ -169,10 +169,10 @@ impl Resolution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the vote associated with this 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 {
|
match self.vote_id {
|
||||||
Some(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))
|
Ok(Some(vote))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
@ -128,8 +128,8 @@ impl Vote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the resolution associated with this 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> {
|
pub fn get_resolution(&self, db: &crate::db::DB) -> Result<Option<super::Resolution>, SledDBError> {
|
||||||
let all_resolutions = db.list()?;
|
let all_resolutions = db.list::<super::Resolution>()?;
|
||||||
let vote_resolution = all_resolutions
|
let vote_resolution = all_resolutions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|resolution| resolution.vote_id == Some(self.id));
|
.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 biz;
|
||||||
pub mod mcc;
|
pub mod mcc;
|
||||||
pub mod circle;
|
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