merge branches and cleanup db
This commit is contained in:
5
herodb_old/.gitignore
vendored
5
herodb_old/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
target/
|
||||
temp/
|
||||
tmp/
|
||||
*.log
|
||||
*.tmp
|
||||
2054
herodb_old/Cargo.lock
generated
2054
herodb_old/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
[package]
|
||||
name = "herodb"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A database library built on top of ourdb with model support"
|
||||
license = "MIT"
|
||||
authors = ["HeroCode Team"]
|
||||
|
||||
[dependencies]
|
||||
ourdb = { path = "../ourdb" }
|
||||
tst = { path = "../tst" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
uuid = { version = "1.3", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
brotli = "3.4"
|
||||
tempfile = "3.8"
|
||||
poem = "1.3.55"
|
||||
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rhai = "1.21.0"
|
||||
paste = "1.0"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
[[example]]
|
||||
name = "rhai_demo"
|
||||
path = "examples/rhai_demo.rs"
|
||||
|
||||
[[example]]
|
||||
name = "business_models_demo"
|
||||
path = "examples/business_models_demo.rs"
|
||||
|
||||
[[example]]
|
||||
name = "ourdb_example"
|
||||
path = "examples/ourdb_example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tst_index_example"
|
||||
path = "examples/tst_index_example.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dbexample_prod"
|
||||
path = "src/cmd/dbexample_prod/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dbexample_mcc"
|
||||
path = "src/cmd/dbexample_mcc/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dbexample_gov"
|
||||
path = "src/cmd/dbexample_gov/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "dbexample_biz"
|
||||
path = "src/cmd/dbexample_biz/main.rs"
|
||||
@@ -1,94 +0,0 @@
|
||||
# HeroDB
|
||||
|
||||
A database library built on top of sled with model support.
|
||||
|
||||
|
||||
|
||||
## example
|
||||
|
||||
```bash
|
||||
#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
|
||||
|
||||
- Type-safe database operations
|
||||
- Builder pattern for model creation
|
||||
- Transaction support
|
||||
- Model-specific convenience methods
|
||||
- Compression for efficient storage
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rust
|
||||
use herodb::db::{DB, DBBuilder};
|
||||
use herodb::models::biz::{Product, ProductBuilder, ProductType, ProductStatus, Currency, CurrencyBuilder};
|
||||
|
||||
// Create a database instance
|
||||
let db = DBBuilder::new("db")
|
||||
.register_model::<Product>()
|
||||
.register_model::<Currency>()
|
||||
.build()
|
||||
.expect("Failed to create database");
|
||||
|
||||
// Create a product using the builder pattern
|
||||
let price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
|
||||
let product = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Premium Service")
|
||||
.description("Our premium service offering")
|
||||
.price(price)
|
||||
.type_(ProductType::Service)
|
||||
.category("Services")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(30)
|
||||
.build()
|
||||
.expect("Failed to build product");
|
||||
|
||||
// Insert the product using the generic method
|
||||
db.set(&product).expect("Failed to insert product");
|
||||
|
||||
// Retrieve the product
|
||||
let retrieved_product = db.get::<Product>(&"1".to_string()).expect("Failed to retrieve product");
|
||||
```
|
||||
|
||||
### Using Model-Specific Convenience Methods
|
||||
|
||||
The library provides model-specific convenience methods for common database operations:
|
||||
|
||||
```rust
|
||||
// Insert a product using the model-specific method
|
||||
db.insert_product(&product).expect("Failed to insert product");
|
||||
|
||||
// Retrieve a product by ID
|
||||
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
|
||||
|
||||
// List all products
|
||||
let all_products = db.list_products().expect("Failed to list products");
|
||||
|
||||
// Delete a product
|
||||
db.delete_product(1).expect("Failed to delete product");
|
||||
```
|
||||
|
||||
These methods are available for all registered models:
|
||||
|
||||
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,156 +0,0 @@
|
||||
|
||||
|
||||
please refactor each of the objects in the the chosen folder to use builder paradigm, see below for an example
|
||||
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 1: Define your struct
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub enum ProductType {
|
||||
Service,
|
||||
// Other variants...
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProductStatus {
|
||||
Available,
|
||||
Unavailable,
|
||||
// Other variants...
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Product {
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
price: f64,
|
||||
product_type: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: u32,
|
||||
validity_days: u32,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 2: Create a builder
|
||||
|
||||
```rust
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<f64>,
|
||||
product_type: Option<ProductType>,
|
||||
category: Option<String>,
|
||||
status: Option<ProductStatus>,
|
||||
max_amount: Option<u32>,
|
||||
validity_days: Option<u32>,
|
||||
}
|
||||
|
||||
impl ProductBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
price: None,
|
||||
product_type: None,
|
||||
category: None,
|
||||
status: None,
|
||||
max_amount: None,
|
||||
validity_days: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn price(mut self, price: f64) -> Self {
|
||||
self.price = Some(price);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn product_type(mut self, product_type: ProductType) -> Self {
|
||||
self.product_type = Some(product_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
|
||||
self.category = Some(category.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: ProductStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_amount(mut self, max_amount: u32) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validity_days(mut self, validity_days: u32) -> Self {
|
||||
self.validity_days = Some(validity_days);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Product, &'static str> {
|
||||
Ok(Product {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
price: self.price.ok_or("price is required")?,
|
||||
product_type: self.product_type.ok_or("type is required")?,
|
||||
category: self.category.ok_or("category is required")?,
|
||||
status: self.status.ok_or("status is required")?,
|
||||
max_amount: self.max_amount.ok_or("max_amount is required")?,
|
||||
validity_days: self.validity_days.ok_or("validity_days is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 3: Use it like this
|
||||
|
||||
```rust
|
||||
let product = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Premium Service")
|
||||
.description("Our premium service offering")
|
||||
.price(99.99)
|
||||
.product_type(ProductType::Service)
|
||||
.category("Services")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(30)
|
||||
.build()
|
||||
.expect("Failed to build product");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This way:
|
||||
- You don’t need to remember the order of parameters.
|
||||
- You get readable, self-documenting code.
|
||||
- It’s easier to provide defaults or optional values if you want later.
|
||||
|
||||
Want help generating this automatically via a macro or just want it shorter? I can show you a derive macro to do that too.
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
make a readme for the chosen folder (module)
|
||||
|
||||
make a dense representation of the objects and how to use them
|
||||
explain what the rootobjects are
|
||||
|
||||
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
|
||||
|
||||
don't explain the low level implementation details like sled, ...
|
||||
@@ -1,994 +0,0 @@
|
||||
# Best Practices for Wrapping Rust Functions with Rhai
|
||||
|
||||
This document provides comprehensive guidance on how to effectively wrap Rust functions with different standard arguments, pass structs, and handle various return types including errors when using the Rhai scripting language.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Basic Function Registration](#basic-function-registration)
|
||||
3. [Working with Different Argument Types](#working-with-different-argument-types)
|
||||
4. [Passing and Working with Structs](#passing-and-working-with-structs)
|
||||
5. [Error Handling](#error-handling)
|
||||
6. [Returning Different Types](#returning-different-types)
|
||||
7. [Native Function Handling](#native-function-handling)
|
||||
8. [Advanced Patterns](#advanced-patterns)
|
||||
9. [Complete Examples](#complete-examples)
|
||||
|
||||
## Introduction
|
||||
|
||||
Rhai is an embedded scripting language for Rust that allows you to expose Rust functions to scripts and vice versa. This document focuses on the best practices for wrapping Rust functions so they can be called from Rhai scripts, with special attention to handling different argument types, structs, and error conditions.
|
||||
|
||||
## Basic Function Registration
|
||||
|
||||
### Simple Function Registration
|
||||
|
||||
The most basic way to register a Rust function with Rhai is using the `register_fn` method:
|
||||
|
||||
```rust
|
||||
fn add(x: i64, y: i64) -> i64 {
|
||||
x + y
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the function with Rhai
|
||||
engine.register_fn("add", add);
|
||||
|
||||
// Now the function can be called from Rhai scripts
|
||||
let result = engine.eval::<i64>("add(40, 2)")?;
|
||||
|
||||
println!("Result: {}", result); // prints 42
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Function Naming Conventions
|
||||
|
||||
When registering functions, follow these naming conventions:
|
||||
|
||||
1. Use snake_case for function names to maintain consistency with Rhai's style
|
||||
2. Choose descriptive names that clearly indicate the function's purpose
|
||||
3. For functions that operate on specific types, consider prefixing with the type name (e.g., `string_length`)
|
||||
|
||||
## Working with Different Argument Types
|
||||
|
||||
### Primitive Types
|
||||
|
||||
Rhai supports the following primitive types that can be directly used as function arguments:
|
||||
|
||||
- `i64` (integer)
|
||||
- `f64` (float)
|
||||
- `bool` (boolean)
|
||||
- `String` or `&str` (string)
|
||||
- `char` (character)
|
||||
- `()` (unit type)
|
||||
|
||||
Example:
|
||||
|
||||
```rust
|
||||
fn calculate(num: i64, factor: f64, enabled: bool) -> f64 {
|
||||
if enabled {
|
||||
num as f64 * factor
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("calculate", calculate);
|
||||
```
|
||||
|
||||
### Arrays and Collections
|
||||
|
||||
For array arguments:
|
||||
|
||||
```rust
|
||||
fn sum_array(arr: Array) -> i64 {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_int().ok())
|
||||
.sum()
|
||||
}
|
||||
|
||||
engine.register_fn("sum_array", sum_array);
|
||||
```
|
||||
|
||||
### Optional Arguments and Function Overloading
|
||||
|
||||
Rhai supports function overloading, which allows you to register multiple functions with the same name but different parameter types or counts:
|
||||
|
||||
```rust
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
fn greet_with_title(title: &str, name: &str) -> String {
|
||||
format!("Hello, {} {}!", title, name)
|
||||
}
|
||||
|
||||
engine.register_fn("greet", greet);
|
||||
engine.register_fn("greet", greet_with_title);
|
||||
|
||||
// In Rhai:
|
||||
// greet("World") -> "Hello, World!"
|
||||
// greet("Mr.", "Smith") -> "Hello, Mr. Smith!"
|
||||
```
|
||||
|
||||
## Passing and Working with Structs
|
||||
|
||||
### Registering Custom Types
|
||||
|
||||
To use Rust structs in Rhai, you need to register them:
|
||||
|
||||
#### Method 1: Using the CustomType Trait (Recommended)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_extra)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
}
|
||||
|
||||
impl TestStruct {
|
||||
pub fn new() -> Self {
|
||||
Self { x: 1 }
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.x += 1000;
|
||||
}
|
||||
|
||||
pub fn calculate(&mut self, data: i64) -> i64 {
|
||||
self.x * data
|
||||
}
|
||||
|
||||
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("TestStruct")
|
||||
.with_fn("new_ts", Self::new)
|
||||
.with_fn("update", Self::update)
|
||||
.with_fn("calc", Self::calculate);
|
||||
}
|
||||
}
|
||||
|
||||
// In your main function:
|
||||
let mut engine = Engine::new();
|
||||
engine.build_type::<TestStruct>();
|
||||
```
|
||||
|
||||
#### Method 2: Manual Registration
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
}
|
||||
|
||||
impl TestStruct {
|
||||
pub fn new() -> Self {
|
||||
Self { x: 1 }
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.x += 1000;
|
||||
}
|
||||
}
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.register_type_with_name::<TestStruct>("TestStruct")
|
||||
.register_fn("new_ts", TestStruct::new)
|
||||
.register_fn("update", TestStruct::update);
|
||||
```
|
||||
|
||||
### Accessing Struct Fields
|
||||
|
||||
By default, Rhai can access public fields of registered structs:
|
||||
|
||||
```rust
|
||||
// In Rhai script:
|
||||
let x = new_ts();
|
||||
x.x = 42; // Direct field access
|
||||
```
|
||||
|
||||
### Passing Structs as Arguments
|
||||
|
||||
When passing structs as arguments to functions, ensure they implement the `Clone` trait:
|
||||
|
||||
```rust
|
||||
fn process_struct(test: TestStruct) -> i64 {
|
||||
test.x * 2
|
||||
}
|
||||
|
||||
engine.register_fn("process_struct", process_struct);
|
||||
```
|
||||
|
||||
### Returning Structs from Functions
|
||||
|
||||
You can return custom structs from functions:
|
||||
|
||||
```rust
|
||||
fn create_struct(value: i64) -> TestStruct {
|
||||
TestStruct { x: value }
|
||||
}
|
||||
|
||||
engine.register_fn("create_struct", create_struct);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Error handling is a critical aspect of integrating Rust functions with Rhai. Proper error handling ensures that script execution fails gracefully with meaningful error messages.
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
The most basic way to handle errors is to return a `Result` type:
|
||||
|
||||
```rust
|
||||
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
if b == 0 {
|
||||
// Return an error if division by zero
|
||||
Err("Division by zero".into())
|
||||
} else {
|
||||
Ok(a / b)
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("divide", divide);
|
||||
```
|
||||
|
||||
### EvalAltResult Types
|
||||
|
||||
Rhai provides several error types through the `EvalAltResult` enum:
|
||||
|
||||
```rust
|
||||
use rhai::EvalAltResult;
|
||||
use rhai::Position;
|
||||
|
||||
fn my_function() -> Result<i64, Box<EvalAltResult>> {
|
||||
// Different error types
|
||||
|
||||
// Runtime error - general purpose error
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Something went wrong".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
|
||||
// Type error - when a type mismatch occurs
|
||||
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
||||
"expected i64, got string".into(),
|
||||
Position::NONE,
|
||||
"i64".into()
|
||||
)));
|
||||
|
||||
// Function not found error
|
||||
return Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
|
||||
"function_name".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error Types
|
||||
|
||||
For more structured error handling, you can create custom error types:
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
use rhai::{EvalAltResult, Position};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum MyError {
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Calculation error: {0}")]
|
||||
CalculationError(String),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(String),
|
||||
}
|
||||
|
||||
// Convert your custom error to EvalAltResult
|
||||
fn process_data(input: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
// Your logic here that might return a custom error
|
||||
let result = validate_input(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Validation failed: {}", e),
|
||||
Position::NONE
|
||||
)))?;
|
||||
|
||||
let processed = calculate(result)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Calculation failed: {}", e),
|
||||
Position::NONE
|
||||
)))?;
|
||||
|
||||
if processed < 0 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Negative result not allowed".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(processed)
|
||||
}
|
||||
|
||||
// Helper functions that return our custom error type
|
||||
fn validate_input(input: i64) -> Result<i64, MyError> {
|
||||
if input <= 0 {
|
||||
return Err(MyError::InvalidInput("Input must be positive".into()));
|
||||
}
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
fn calculate(value: i64) -> Result<i64, MyError> {
|
||||
if value > 1000 {
|
||||
return Err(MyError::CalculationError("Value too large".into()));
|
||||
}
|
||||
Ok(value * 2)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Propagation
|
||||
|
||||
When calling Rhai functions from Rust, errors are propagated through the `?` operator:
|
||||
|
||||
```rust
|
||||
let result = engine.eval::<i64>("divide(10, 0)")?; // This will propagate the error
|
||||
```
|
||||
|
||||
### Error Context and Position Information
|
||||
|
||||
For better debugging, include position information in your errors:
|
||||
|
||||
```rust
|
||||
fn parse_config(config: &str) -> Result<Map, Box<EvalAltResult>> {
|
||||
// Get the call position from the context
|
||||
let pos = Position::NONE; // In a real function, you'd get this from NativeCallContext
|
||||
|
||||
match serde_json::from_str::<serde_json::Value>(config) {
|
||||
Ok(json) => {
|
||||
// Convert JSON to Rhai Map
|
||||
let mut map = Map::new();
|
||||
// ... conversion logic ...
|
||||
Ok(map)
|
||||
},
|
||||
Err(e) => {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to parse config: {}", e),
|
||||
pos
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices for Error Handling
|
||||
|
||||
1. **Be Specific**: Provide clear, specific error messages that help script writers understand what went wrong
|
||||
2. **Include Context**: When possible, include relevant context in error messages (e.g., variable values, expected types)
|
||||
3. **Consistent Error Types**: Use consistent error types for similar issues
|
||||
4. **Validate Early**: Validate inputs at the beginning of functions to fail fast
|
||||
5. **Document Error Conditions**: Document possible error conditions for functions exposed to Rhai
|
||||
|
||||
|
||||
## Returning Different Types
|
||||
|
||||
Properly handling return types is crucial for creating a seamless integration between Rust and Rhai. This section covers various approaches to returning different types of data from Rust functions to Rhai scripts.
|
||||
|
||||
### Simple Return Types
|
||||
|
||||
For simple return types, specify the type when registering the function:
|
||||
|
||||
```rust
|
||||
fn get_number() -> i64 { 42 }
|
||||
fn get_string() -> String { "hello".to_string() }
|
||||
fn get_boolean() -> bool { true }
|
||||
fn get_float() -> f64 { 3.14159 }
|
||||
fn get_char() -> char { 'A' }
|
||||
fn get_unit() -> () { () }
|
||||
|
||||
engine.register_fn("get_number", get_number);
|
||||
engine.register_fn("get_string", get_string);
|
||||
engine.register_fn("get_boolean", get_boolean);
|
||||
engine.register_fn("get_float", get_float);
|
||||
engine.register_fn("get_char", get_char);
|
||||
engine.register_fn("get_unit", get_unit);
|
||||
```
|
||||
|
||||
### Dynamic Return Types
|
||||
|
||||
WE SHOULD TRY NOT TO DO THIS
|
||||
|
||||
For functions that may return different types based on conditions, use the `Dynamic` type:
|
||||
|
||||
```rust
|
||||
fn get_value(which: i64) -> Dynamic {
|
||||
match which {
|
||||
0 => Dynamic::from(42),
|
||||
1 => Dynamic::from("hello"),
|
||||
2 => Dynamic::from(true),
|
||||
3 => Dynamic::from(3.14159),
|
||||
4 => {
|
||||
let mut array = Array::new();
|
||||
array.push(Dynamic::from(1));
|
||||
array.push(Dynamic::from(2));
|
||||
Dynamic::from_array(array)
|
||||
},
|
||||
5 => {
|
||||
let mut map = Map::new();
|
||||
map.insert("key".into(), "value".into());
|
||||
Dynamic::from_map(map)
|
||||
},
|
||||
_ => Dynamic::UNIT,
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("get_value", get_value);
|
||||
```
|
||||
|
||||
### Returning Collections
|
||||
|
||||
Rhai supports various collection types:
|
||||
|
||||
```rust
|
||||
// Returning an array
|
||||
fn get_array() -> Array {
|
||||
let mut array = Array::new();
|
||||
array.push(Dynamic::from(1));
|
||||
array.push(Dynamic::from("hello"));
|
||||
array.push(Dynamic::from(true));
|
||||
array
|
||||
}
|
||||
|
||||
// Returning a map
|
||||
fn get_map() -> Map {
|
||||
let mut map = Map::new();
|
||||
map.insert("number".into(), 42.into());
|
||||
map.insert("string".into(), "hello".into());
|
||||
map.insert("boolean".into(), true.into());
|
||||
map
|
||||
}
|
||||
|
||||
// Returning a typed Vec (will be converted to Rhai Array)
|
||||
fn get_numbers() -> Vec<i64> {
|
||||
vec![1, 2, 3, 4, 5]
|
||||
}
|
||||
|
||||
// Returning a HashMap (will be converted to Rhai Map)
|
||||
fn get_config() -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("host".to_string(), "localhost".to_string());
|
||||
map.insert("port".to_string(), "8080".to_string());
|
||||
map
|
||||
}
|
||||
|
||||
engine.register_fn("get_array", get_array);
|
||||
engine.register_fn("get_map", get_map);
|
||||
engine.register_fn("get_numbers", get_numbers);
|
||||
engine.register_fn("get_config", get_config);
|
||||
```
|
||||
|
||||
### Returning Custom Structs
|
||||
|
||||
For returning custom structs, ensure they implement the `Clone` trait:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
name: String,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
fn create_struct(value: i64, name: &str, active: bool) -> TestStruct {
|
||||
TestStruct {
|
||||
x: value,
|
||||
name: name.to_string(),
|
||||
active
|
||||
}
|
||||
}
|
||||
|
||||
fn get_struct_array() -> Vec<TestStruct> {
|
||||
vec![
|
||||
TestStruct { x: 1, name: "one".to_string(), active: true },
|
||||
TestStruct { x: 2, name: "two".to_string(), active: false },
|
||||
]
|
||||
}
|
||||
|
||||
engine.register_type_with_name::<TestStruct>("TestStruct")
|
||||
.register_fn("create_struct", create_struct)
|
||||
.register_fn("get_struct_array", get_struct_array);
|
||||
```
|
||||
|
||||
### Returning Results and Options
|
||||
|
||||
For functions that might fail or return optional values:
|
||||
|
||||
```rust
|
||||
// Returning a Result
|
||||
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
if b == 0 {
|
||||
Err("Division by zero".into())
|
||||
} else {
|
||||
Ok(a / b)
|
||||
}
|
||||
}
|
||||
|
||||
// Returning an Option (converted to Dynamic)
|
||||
fn find_item(id: i64) -> Dynamic {
|
||||
let item = lookup_item(id);
|
||||
|
||||
match item {
|
||||
Some(value) => value.into(),
|
||||
None => Dynamic::UNIT, // Rhai has no null, so use () for None
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function returning Option
|
||||
fn lookup_item(id: i64) -> Option<TestStruct> {
|
||||
match id {
|
||||
1 => Some(TestStruct { x: 1, name: "one".to_string(), active: true }),
|
||||
2 => Some(TestStruct { x: 2, name: "two".to_string(), active: false }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("divide", divide);
|
||||
engine.register_fn("find_item", find_item);
|
||||
```
|
||||
|
||||
### Serialization and Deserialization
|
||||
|
||||
When working with JSON or other serialized formats:
|
||||
|
||||
```rust
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
|
||||
// Return JSON data as a Rhai Map
|
||||
fn get_json_data() -> Result<Map, Box<EvalAltResult>> {
|
||||
// Simulate fetching JSON data
|
||||
let json_data = json!({
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"address": {
|
||||
"street": "123 Main St",
|
||||
"city": "Anytown"
|
||||
},
|
||||
"phones": ["+1-555-1234", "+1-555-5678"]
|
||||
});
|
||||
|
||||
// Convert JSON to Rhai Map
|
||||
json_to_rhai_value(json_data)
|
||||
.and_then(|v| v.try_cast::<Map>().map_err(|_| "Expected a map".into()))
|
||||
}
|
||||
|
||||
// Helper function to convert JSON Value to Rhai Dynamic
|
||||
fn json_to_rhai_value(json: JsonValue) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match json {
|
||||
JsonValue::Null => Ok(Dynamic::UNIT),
|
||||
JsonValue::Bool(b) => Ok(b.into()),
|
||||
JsonValue::Number(n) => {
|
||||
if n.is_i64() {
|
||||
Ok(n.as_i64().unwrap().into())
|
||||
} else {
|
||||
Ok(n.as_f64().unwrap().into())
|
||||
}
|
||||
},
|
||||
JsonValue::String(s) => Ok(s.into()),
|
||||
JsonValue::Array(arr) => {
|
||||
let mut rhai_array = Array::new();
|
||||
for item in arr {
|
||||
rhai_array.push(json_to_rhai_value(item)?);
|
||||
}
|
||||
Ok(Dynamic::from_array(rhai_array))
|
||||
},
|
||||
JsonValue::Object(obj) => {
|
||||
let mut rhai_map = Map::new();
|
||||
for (k, v) in obj {
|
||||
rhai_map.insert(k.into(), json_to_rhai_value(v)?);
|
||||
}
|
||||
Ok(Dynamic::from_map(rhai_map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("get_json_data", get_json_data);
|
||||
```
|
||||
|
||||
### Working with Dynamic Type System
|
||||
|
||||
Understanding how to work with Rhai's Dynamic type system is essential:
|
||||
|
||||
```rust
|
||||
// Function that examines a Dynamic value and returns information about it
|
||||
fn inspect_value(value: Dynamic) -> Map {
|
||||
let mut info = Map::new();
|
||||
|
||||
// Store the type name
|
||||
info.insert("type".into(), value.type_name().into());
|
||||
|
||||
// Store specific type information
|
||||
if value.is_int() {
|
||||
info.insert("category".into(), "number".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_float() {
|
||||
info.insert("category".into(), "number".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_string() {
|
||||
info.insert("category".into(), "string".into());
|
||||
info.insert("length".into(), value.clone_cast::<String>().len().into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_array() {
|
||||
info.insert("category".into(), "array".into());
|
||||
info.insert("length".into(), value.clone_cast::<Array>().len().into());
|
||||
} else if value.is_map() {
|
||||
info.insert("category".into(), "map".into());
|
||||
info.insert("keys".into(), value.clone_cast::<Map>().keys().len().into());
|
||||
} else if value.is_bool() {
|
||||
info.insert("category".into(), "boolean".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else {
|
||||
info.insert("category".into(), "other".into());
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
engine.register_fn("inspect", inspect_value);
|
||||
```
|
||||
|
||||
## Native Function Handling
|
||||
|
||||
When working with native Rust functions in Rhai, there are several important considerations for handling different argument types, especially when dealing with complex data structures and error cases.
|
||||
|
||||
### Native Function Signature
|
||||
|
||||
Native Rust functions registered with Rhai can have one of two signatures:
|
||||
|
||||
1. **Standard Function Signature**: Functions with typed parameters
|
||||
```rust
|
||||
fn my_function(param1: Type1, param2: Type2, ...) -> ReturnType { ... }
|
||||
```
|
||||
|
||||
2. **Dynamic Function Signature**: Functions that handle raw Dynamic values
|
||||
```rust
|
||||
fn my_dynamic_function(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> { ... }
|
||||
```
|
||||
|
||||
### Working with Raw Dynamic Arguments
|
||||
|
||||
The dynamic function signature gives you more control but requires manual type checking and conversion:
|
||||
|
||||
```rust
|
||||
fn process_dynamic_args(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check number of arguments
|
||||
if args.len() != 2 {
|
||||
return Err("Expected exactly 2 arguments".into());
|
||||
}
|
||||
|
||||
// Extract and convert the first argument to an integer
|
||||
let arg1 = args[0].as_int().map_err(|_| "First argument must be an integer".into())?;
|
||||
|
||||
// Extract and convert the second argument to a string
|
||||
let arg2 = args[1].as_str().map_err(|_| "Second argument must be a string".into())?;
|
||||
|
||||
// Process the arguments
|
||||
let result = format!("{}: {}", arg2, arg1);
|
||||
|
||||
// Return the result as a Dynamic value
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
// Register the function
|
||||
engine.register_fn("process", process_dynamic_args);
|
||||
```
|
||||
|
||||
### Handling Complex Struct Arguments
|
||||
|
||||
When working with complex struct arguments, you have several options:
|
||||
|
||||
#### Option 1: Use typed parameters (recommended for simple cases)
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
struct ComplexData {
|
||||
id: i64,
|
||||
values: Vec<f64>,
|
||||
}
|
||||
|
||||
fn process_complex(data: &mut ComplexData, factor: f64) -> f64 {
|
||||
let sum: f64 = data.values.iter().sum();
|
||||
data.values.push(sum * factor);
|
||||
sum * factor
|
||||
}
|
||||
|
||||
engine.register_fn("process_complex", process_complex);
|
||||
```
|
||||
|
||||
#### Option 2: Use Dynamic parameters for more flexibility
|
||||
|
||||
```rust
|
||||
fn process_complex_dynamic(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check arguments
|
||||
if args.len() != 2 {
|
||||
return Err("Expected exactly 2 arguments".into());
|
||||
}
|
||||
|
||||
// Get mutable reference to the complex data
|
||||
let data = args[0].write_lock::<ComplexData>()
|
||||
.ok_or_else(|| "First argument must be ComplexData".into())?;
|
||||
|
||||
// Get the factor
|
||||
let factor = args[1].as_float().map_err(|_| "Second argument must be a number".into())?;
|
||||
|
||||
// Process the data
|
||||
let sum: f64 = data.values.iter().sum();
|
||||
data.values.push(sum * factor);
|
||||
|
||||
Ok((sum * factor).into())
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Variable Arguments
|
||||
|
||||
For functions that accept a variable number of arguments:
|
||||
|
||||
```rust
|
||||
fn sum_all(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let mut total: i64 = 0;
|
||||
|
||||
for arg in args.iter() {
|
||||
total += arg.as_int().map_err(|_| "All arguments must be integers".into())?;
|
||||
}
|
||||
|
||||
Ok(total.into())
|
||||
}
|
||||
|
||||
engine.register_fn("sum_all", sum_all);
|
||||
|
||||
// In Rhai:
|
||||
// sum_all(1, 2, 3, 4, 5) -> 15
|
||||
// sum_all(10, 20) -> 30
|
||||
```
|
||||
|
||||
### Handling Optional Arguments
|
||||
|
||||
For functions with optional arguments, use function overloading:
|
||||
|
||||
```rust
|
||||
fn create_person(name: &str) -> Person {
|
||||
Person { name: name.to_string(), age: 30 } // Default age
|
||||
}
|
||||
|
||||
fn create_person_with_age(name: &str, age: i64) -> Person {
|
||||
Person { name: name.to_string(), age }
|
||||
}
|
||||
|
||||
engine.register_fn("create_person", create_person);
|
||||
engine.register_fn("create_person", create_person_with_age);
|
||||
|
||||
// In Rhai:
|
||||
// create_person("John") -> Person with name "John" and age 30
|
||||
// create_person("John", 25) -> Person with name "John" and age 25
|
||||
```
|
||||
|
||||
### Handling Default Arguments
|
||||
|
||||
Rhai doesn't directly support default arguments, but you can simulate them:
|
||||
|
||||
```rust
|
||||
fn configure(options: &mut Map) -> Result<(), Box<EvalAltResult>> {
|
||||
// Check if certain options exist, if not, set defaults
|
||||
if !options.contains_key("timeout") {
|
||||
options.insert("timeout".into(), 30_i64.into());
|
||||
}
|
||||
|
||||
if !options.contains_key("retry") {
|
||||
options.insert("retry".into(), true.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
engine.register_fn("configure", configure);
|
||||
|
||||
// In Rhai:
|
||||
// let options = #{};
|
||||
// configure(options);
|
||||
// print(options.timeout); // Prints 30
|
||||
```
|
||||
|
||||
### Handling Mutable and Immutable References
|
||||
|
||||
Rhai supports both mutable and immutable references:
|
||||
|
||||
```rust
|
||||
// Function taking an immutable reference
|
||||
fn get_name(person: &Person) -> String {
|
||||
person.name.clone()
|
||||
}
|
||||
|
||||
// Function taking a mutable reference
|
||||
fn increment_age(person: &mut Person) {
|
||||
person.age += 1;
|
||||
}
|
||||
|
||||
engine.register_fn("get_name", get_name);
|
||||
engine.register_fn("increment_age", increment_age);
|
||||
```
|
||||
|
||||
### Converting Between Rust and Rhai Types
|
||||
|
||||
When you need to convert between Rust and Rhai types:
|
||||
|
||||
```rust
|
||||
// Convert a Rust HashMap to a Rhai Map
|
||||
fn create_config() -> Map {
|
||||
let mut rust_map = HashMap::new();
|
||||
rust_map.insert("server".to_string(), "localhost".to_string());
|
||||
rust_map.insert("port".to_string(), "8080".to_string());
|
||||
|
||||
// Convert to Rhai Map
|
||||
let mut rhai_map = Map::new();
|
||||
for (k, v) in rust_map {
|
||||
rhai_map.insert(k.into(), v.into());
|
||||
}
|
||||
|
||||
rhai_map
|
||||
}
|
||||
|
||||
// Convert a Rhai Array to a Rust Vec
|
||||
fn process_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
|
||||
// Convert to Rust Vec<i64>
|
||||
let rust_vec: Result<Vec<i64>, _> = arr.iter()
|
||||
.map(|v| v.as_int().map_err(|_| "Array must contain only integers".into()))
|
||||
.collect();
|
||||
|
||||
let numbers = rust_vec?;
|
||||
Ok(numbers.iter().sum())
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Basic Function Registration and Struct Handling
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, EvalAltResult, RegisterFn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: i64,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn new(name: &str, age: i64) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
age,
|
||||
}
|
||||
}
|
||||
|
||||
fn greet(&self) -> String {
|
||||
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
|
||||
}
|
||||
|
||||
fn have_birthday(&mut self) {
|
||||
self.age += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_adult(person: &Person) -> bool {
|
||||
person.age >= 18
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the Person type
|
||||
engine
|
||||
.register_type_with_name::<Person>("Person")
|
||||
.register_fn("new_person", Person::new)
|
||||
.register_fn("greet", Person::greet)
|
||||
.register_fn("have_birthday", Person::have_birthday)
|
||||
.register_fn("is_adult", is_adult);
|
||||
|
||||
// Run a script that uses the Person type
|
||||
let result = engine.eval::<String>(r#"
|
||||
let p = new_person("John", 17);
|
||||
let greeting = p.greet();
|
||||
|
||||
if !is_adult(p) {
|
||||
p.have_birthday();
|
||||
}
|
||||
|
||||
greeting + " Now I am " + p.age.to_string() + " years old."
|
||||
"#)?;
|
||||
|
||||
println!("{}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Error Handling and Complex Return Types
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, EvalAltResult, Map, Dynamic};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Product {
|
||||
id: i64,
|
||||
name: String,
|
||||
price: f64,
|
||||
}
|
||||
|
||||
fn get_product(id: i64) -> Result<Product, Box<EvalAltResult>> {
|
||||
match id {
|
||||
1 => Ok(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }),
|
||||
2 => Ok(Product { id: 2, name: "Phone".to_string(), price: 499.99 }),
|
||||
_ => Err("Product not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_total(products: Array) -> Result<f64, Box<EvalAltResult>> {
|
||||
let mut total = 0.0;
|
||||
|
||||
for product_dynamic in products.iter() {
|
||||
let product = product_dynamic.clone().try_cast::<Product>()
|
||||
.map_err(|_| "Invalid product in array".into())?;
|
||||
|
||||
total += product.price;
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn get_product_map() -> Map {
|
||||
let mut map = Map::new();
|
||||
|
||||
map.insert("laptop".into(),
|
||||
Dynamic::from(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }));
|
||||
map.insert("phone".into(),
|
||||
Dynamic::from(Product { id: 2, name: "Phone".to_string(), price: 499.99 }));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.register_type_with_name::<Product>("Product")
|
||||
.register_fn("get_product", get_product)
|
||||
.register_fn("calculate_total", calculate_total)
|
||||
.register_fn("get_product_map", get_product_map);
|
||||
|
||||
let result = engine.eval::<f64>(r#"
|
||||
let products = [];
|
||||
|
||||
// Try to get products
|
||||
try {
|
||||
products.push(get_product(1));
|
||||
products.push(get_product(2));
|
||||
products.push(get_product(3)); // This will throw an error
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`);
|
||||
}
|
||||
|
||||
// Get products from map
|
||||
let product_map = get_product_map();
|
||||
products.push(product_map.laptop);
|
||||
|
||||
calculate_total(products)
|
||||
"#)?;
|
||||
|
||||
println!("Total: ${:.2}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
@@ -1,134 +0,0 @@
|
||||
|
||||
### Error Handling in Dynamic Functions
|
||||
|
||||
When working with the dynamic function signature, error handling is slightly different:
|
||||
|
||||
```rust
|
||||
fn dynamic_function(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Get the position information from the context
|
||||
let pos = ctx.position();
|
||||
|
||||
// Validate arguments
|
||||
if args.len() < 2 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Expected at least 2 arguments, got {}", args.len()),
|
||||
pos
|
||||
)));
|
||||
}
|
||||
|
||||
// Try to convert arguments with proper error handling
|
||||
let arg1 = match args[0].as_int() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
||||
"Expected first argument to be an integer".into(),
|
||||
pos,
|
||||
"i64".into()
|
||||
)))
|
||||
};
|
||||
|
||||
// Process with error handling
|
||||
if arg1 <= 0 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"First argument must be positive".into(),
|
||||
pos
|
||||
)));
|
||||
}
|
||||
|
||||
// Return success
|
||||
Ok(Dynamic::from(arg1 * 2))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Working with Function Pointers
|
||||
|
||||
You can create function pointers that bind to Rust functions:
|
||||
|
||||
```rust
|
||||
fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check number of arguments
|
||||
if args.len() != 2 {
|
||||
return Err("one argument is required, plus the object".into());
|
||||
}
|
||||
|
||||
// Get call arguments
|
||||
let x = args[1].try_cast::<i64>().map_err(|_| "argument must be an integer".into())?;
|
||||
|
||||
// Get mutable reference to the object map, which is passed as the first argument
|
||||
let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?;
|
||||
|
||||
// Do something awesome here ...
|
||||
let result = x * 2;
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
// Register a function to create a pre-defined object
|
||||
engine.register_fn("create_awesome_object", || {
|
||||
// Use an object map as base
|
||||
let mut map = Map::new();
|
||||
|
||||
// Create a function pointer that binds to 'my_awesome_fn'
|
||||
let fp = FnPtr::from_fn("awesome", my_awesome_fn)?;
|
||||
// ^ name of method
|
||||
// ^ native function
|
||||
|
||||
// Store the function pointer in the object map
|
||||
map.insert("awesome".into(), fp.into());
|
||||
|
||||
Ok(Dynamic::from_map(map))
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Rust Closures from Rhai Functions
|
||||
|
||||
You can encapsulate a Rhai script as a Rust closure:
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, Func};
|
||||
|
||||
let engine = Engine::new();
|
||||
|
||||
let script = "fn calc(x, y) { x + y.len < 42 }";
|
||||
|
||||
// Create a Rust closure from a Rhai function
|
||||
let func = Func::<(i64, &str), bool>::create_from_script(
|
||||
engine, // the 'Engine' is consumed into the closure
|
||||
script, // the script
|
||||
"calc" // the entry-point function name
|
||||
)?;
|
||||
|
||||
// Call the closure
|
||||
let result = func(123, "hello")?;
|
||||
|
||||
// Pass it as a callback to another function
|
||||
schedule_callback(func);
|
||||
```
|
||||
|
||||
### Calling Rhai Functions from Rust
|
||||
|
||||
You can call Rhai functions from Rust:
|
||||
|
||||
```rust
|
||||
// Compile the script to AST
|
||||
let ast = engine.compile(script)?;
|
||||
|
||||
// Create a custom 'Scope'
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Add variables to the scope
|
||||
scope.push("my_var", 42_i64);
|
||||
scope.push("my_string", "hello, world!");
|
||||
scope.push_constant("MY_CONST", true);
|
||||
|
||||
// Call a function defined in the script
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ("abc", 123_i64))?;
|
||||
|
||||
// For a function with one parameter, use a tuple with a trailing comma
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", (123_i64,))?;
|
||||
|
||||
// For a function with no parameters
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ())?;
|
||||
```
|
||||
@@ -1,187 +0,0 @@
|
||||
## Best Practices and Optimization
|
||||
|
||||
When wrapping Rust functions for use with Rhai, following these best practices will help you create efficient, maintainable, and robust code.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Minimize Cloning**: Rhai often requires cloning data, but you can minimize this overhead:
|
||||
```rust
|
||||
// Prefer immutable references when possible
|
||||
fn process_data(data: &MyStruct) -> i64 {
|
||||
// Work with data without cloning
|
||||
data.value * 2
|
||||
}
|
||||
|
||||
// Use mutable references for in-place modifications
|
||||
fn update_data(data: &mut MyStruct) {
|
||||
data.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Avoid Excessive Type Conversions**: Converting between Rhai's Dynamic type and Rust types has overhead:
|
||||
```rust
|
||||
// Inefficient - multiple conversions
|
||||
fn process_inefficient(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let value = args[0].as_int()?;
|
||||
let result = value * 2;
|
||||
Ok(Dynamic::from(result))
|
||||
}
|
||||
|
||||
// More efficient - use typed parameters when possible
|
||||
fn process_efficient(value: i64) -> i64 {
|
||||
value * 2
|
||||
}
|
||||
```
|
||||
|
||||
3. **Batch Operations**: For operations on collections, batch processing is more efficient:
|
||||
```rust
|
||||
// Process an entire array at once rather than element by element
|
||||
fn sum_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
|
||||
arr.iter()
|
||||
.map(|v| v.as_int())
|
||||
.collect::<Result<Vec<i64>, _>>()
|
||||
.map(|nums| nums.iter().sum())
|
||||
.map_err(|_| "Array must contain only integers".into())
|
||||
}
|
||||
```
|
||||
|
||||
4. **Compile Scripts Once**: Reuse compiled ASTs for scripts that are executed multiple times:
|
||||
```rust
|
||||
// Compile once
|
||||
let ast = engine.compile(script)?;
|
||||
|
||||
// Execute multiple times with different parameters
|
||||
for i in 0..10 {
|
||||
let result = engine.eval_ast::<i64>(&ast)?;
|
||||
println!("Result {}: {}", i, result);
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
1. **Use Sync Mode When Needed**: If you need thread safety, use the `sync` feature:
|
||||
```rust
|
||||
// In Cargo.toml
|
||||
// rhai = { version = "1.x", features = ["sync"] }
|
||||
|
||||
// This creates a thread-safe engine
|
||||
let engine = Engine::new();
|
||||
|
||||
// Now you can safely share the engine between threads
|
||||
std::thread::spawn(move || {
|
||||
let result = engine.eval::<i64>("40 + 2")?;
|
||||
println!("Result: {}", result);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Clone the Engine for Multiple Threads**: When not using `sync`, clone the engine for each thread:
|
||||
```rust
|
||||
let engine = Engine::new();
|
||||
|
||||
let handles: Vec<_> = (0..5).map(|i| {
|
||||
let engine_clone = engine.clone();
|
||||
std::thread::spawn(move || {
|
||||
let result = engine_clone.eval::<i64>(&format!("{} + 2", i * 10))?;
|
||||
println!("Thread {}: {}", i, result);
|
||||
})
|
||||
}).collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
1. **Control Scope Size**: Be mindful of the size of your scopes:
|
||||
```rust
|
||||
// Create a new scope for each operation to avoid memory buildup
|
||||
for item in items {
|
||||
let mut scope = Scope::new();
|
||||
scope.push("item", item);
|
||||
engine.eval_with_scope::<()>(&mut scope, "process(item)")?;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Limit Script Complexity**: Use engine options to limit script complexity:
|
||||
```rust
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Set limits to prevent scripts from consuming too many resources
|
||||
engine.set_max_expr_depths(64, 64) // Max expression/statement depth
|
||||
.set_max_function_expr_depth(64) // Max function depth
|
||||
.set_max_array_size(10000) // Max array size
|
||||
.set_max_map_size(10000) // Max map size
|
||||
.set_max_string_size(10000) // Max string size
|
||||
.set_max_call_levels(64); // Max call stack depth
|
||||
```
|
||||
|
||||
3. **Use Shared Values Carefully**: Shared values (via closures) have reference-counting overhead:
|
||||
```rust
|
||||
// Avoid unnecessary capturing in closures when possible
|
||||
engine.register_fn("process", |x: i64| x * 2);
|
||||
|
||||
// Instead of capturing large data structures
|
||||
let large_data = vec![1, 2, 3, /* ... thousands of items ... */];
|
||||
engine.register_fn("process_data", move |idx: i64| {
|
||||
if idx >= 0 && (idx as usize) < large_data.len() {
|
||||
large_data[idx as usize]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
|
||||
// Consider registering a lookup function instead
|
||||
let large_data = std::sync::Arc::new(vec![1, 2, 3, /* ... thousands of items ... */]);
|
||||
let data_ref = large_data.clone();
|
||||
engine.register_fn("lookup", move |idx: i64| {
|
||||
if idx >= 0 && (idx as usize) < data_ref.len() {
|
||||
data_ref[idx as usize]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### API Design
|
||||
|
||||
1. **Consistent Naming**: Use consistent naming conventions:
|
||||
```rust
|
||||
// Good: Consistent naming pattern
|
||||
engine.register_fn("create_user", create_user)
|
||||
.register_fn("update_user", update_user)
|
||||
.register_fn("delete_user", delete_user);
|
||||
|
||||
// Bad: Inconsistent naming
|
||||
engine.register_fn("create_user", create_user)
|
||||
.register_fn("user_update", update_user)
|
||||
.register_fn("remove", delete_user);
|
||||
```
|
||||
|
||||
2. **Logical Function Grouping**: Group related functions together:
|
||||
```rust
|
||||
// Register all string-related functions together
|
||||
engine.register_fn("str_length", |s: &str| s.len() as i64)
|
||||
.register_fn("str_uppercase", |s: &str| s.to_uppercase())
|
||||
.register_fn("str_lowercase", |s: &str| s.to_lowercase());
|
||||
|
||||
// Register all math-related functions together
|
||||
engine.register_fn("math_sin", |x: f64| x.sin())
|
||||
.register_fn("math_cos", |x: f64| x.cos())
|
||||
.register_fn("math_tan", |x: f64| x.tan());
|
||||
```
|
||||
|
||||
3. **Comprehensive Documentation**: Document your API thoroughly:
|
||||
```rust
|
||||
// Add documentation for script writers
|
||||
let mut engine = Engine::new();
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
{
|
||||
// Add function documentation
|
||||
engine.register_fn("calculate_tax", calculate_tax)
|
||||
.register_fn_metadata("calculate_tax", |metadata| {
|
||||
metadata.set_doc_comment("Calculates tax based on income and rate.\n\nParameters:\n- income: Annual income\n- rate: Tax rate (0.0-1.0)\n\nReturns: Calculated tax amount");
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -1,428 +0,0 @@
|
||||
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,108 +0,0 @@
|
||||
// This example demonstrates the basic functionality of the circle models
|
||||
// without using the database functionality
|
||||
|
||||
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Circle Module Basic Demo");
|
||||
|
||||
// Create a circle
|
||||
let circle = Circle::new(
|
||||
1,
|
||||
"ThreeFold Community".to_string(),
|
||||
"A circle for ThreeFold community members".to_string(),
|
||||
);
|
||||
|
||||
println!("Created circle: {:?}", circle);
|
||||
|
||||
// Create members
|
||||
let mut alice = Member::new(
|
||||
1,
|
||||
"Alice".to_string(),
|
||||
"Core contributor".to_string(),
|
||||
Role::Admin,
|
||||
);
|
||||
alice.add_email("alice@example.com".to_string());
|
||||
|
||||
let mut bob = Member::new(
|
||||
2,
|
||||
"Bob".to_string(),
|
||||
"Community member".to_string(),
|
||||
Role::Member,
|
||||
);
|
||||
bob.add_email("bob@example.com".to_string());
|
||||
|
||||
println!("Created members: {:?}, {:?}", alice, bob);
|
||||
|
||||
// Create a domain name
|
||||
let mut domain = Name::new(
|
||||
1,
|
||||
"threefold.io".to_string(),
|
||||
"ThreeFold main domain".to_string(),
|
||||
);
|
||||
|
||||
let record = Record {
|
||||
name: "www".to_string(),
|
||||
text: "ThreeFold Website".to_string(),
|
||||
category: RecordType::A,
|
||||
addr: vec!["192.168.1.1".to_string()],
|
||||
};
|
||||
|
||||
domain.add_record(record);
|
||||
domain.add_admin("alice_pubkey".to_string());
|
||||
|
||||
println!("Created domain: {:?}", domain);
|
||||
|
||||
// Create wallets
|
||||
let mut alice_wallet = Wallet::new(
|
||||
1,
|
||||
"Alice's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GALICE...".to_string(),
|
||||
);
|
||||
|
||||
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||
|
||||
let mut bob_wallet = Wallet::new(
|
||||
2,
|
||||
"Bob's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GBOB...".to_string(),
|
||||
);
|
||||
|
||||
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||
|
||||
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||
|
||||
// Link wallets to members
|
||||
alice.link_wallet(alice_wallet.id);
|
||||
bob.link_wallet(bob_wallet.id);
|
||||
|
||||
println!("Linked wallets to members");
|
||||
|
||||
// Demonstrate wallet operations
|
||||
println!("\nDemonstrating wallet operations:");
|
||||
|
||||
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||
|
||||
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
println!("\nCircle basic demo completed successfully!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Circle Models Demo (In-Memory Version)");
|
||||
println!("=====================================\n");
|
||||
|
||||
// Create a circle
|
||||
let circle = Circle::new(
|
||||
1,
|
||||
"ThreeFold Community".to_string(),
|
||||
"A circle for ThreeFold community members".to_string(),
|
||||
);
|
||||
|
||||
println!("Created circle: {:?}", circle);
|
||||
|
||||
// Create members
|
||||
let mut alice = Member::new(
|
||||
1,
|
||||
"Alice".to_string(),
|
||||
"Core contributor".to_string(),
|
||||
Role::Admin,
|
||||
);
|
||||
alice.add_email("alice@example.com".to_string());
|
||||
|
||||
let mut bob = Member::new(
|
||||
2,
|
||||
"Bob".to_string(),
|
||||
"Community member".to_string(),
|
||||
Role::Member,
|
||||
);
|
||||
bob.add_email("bob@example.com".to_string());
|
||||
|
||||
println!("Created members: {:?}, {:?}", alice, bob);
|
||||
|
||||
// Create a domain name
|
||||
let mut domain = Name::new(
|
||||
1,
|
||||
"threefold.io".to_string(),
|
||||
"ThreeFold main domain".to_string(),
|
||||
);
|
||||
|
||||
let record = Record {
|
||||
name: "www".to_string(),
|
||||
text: "ThreeFold Website".to_string(),
|
||||
category: RecordType::A,
|
||||
addr: vec!["192.168.1.1".to_string()],
|
||||
};
|
||||
|
||||
domain.add_record(record);
|
||||
domain.add_admin("alice_pubkey".to_string());
|
||||
|
||||
println!("Created domain: {:?}", domain);
|
||||
|
||||
// Create wallets
|
||||
let mut alice_wallet = Wallet::new(
|
||||
1,
|
||||
"Alice's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GALICE...".to_string(),
|
||||
);
|
||||
|
||||
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||
|
||||
let mut bob_wallet = Wallet::new(
|
||||
2,
|
||||
"Bob's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GBOB...".to_string(),
|
||||
);
|
||||
|
||||
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||
|
||||
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||
|
||||
// Link wallets to members
|
||||
alice.link_wallet(alice_wallet.id);
|
||||
bob.link_wallet(bob_wallet.id);
|
||||
|
||||
println!("Linked wallets to members");
|
||||
|
||||
// Display all data
|
||||
println!("\nDisplaying all data:");
|
||||
|
||||
println!("Circles: {:#?}", vec![circle]);
|
||||
println!("Members: {:#?}", vec![alice.clone(), bob.clone()]);
|
||||
println!("Names: {:#?}", vec![domain]);
|
||||
println!("Wallets: {:#?}", vec![alice_wallet.clone(), bob_wallet.clone()]);
|
||||
|
||||
// Demonstrate wallet operations
|
||||
println!("\nDemonstrating wallet operations:");
|
||||
|
||||
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||
|
||||
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
println!("\nCircle models demo completed successfully!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//! This is a standalone example that demonstrates the circle models
|
||||
//! without using any database functionality.
|
||||
|
||||
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
|
||||
|
||||
fn main() {
|
||||
println!("Circle Module Standalone Demo");
|
||||
|
||||
// Create a circle
|
||||
let circle = Circle::new(
|
||||
1,
|
||||
"ThreeFold Community".to_string(),
|
||||
"A circle for ThreeFold community members".to_string(),
|
||||
);
|
||||
|
||||
println!("Created circle: {:?}", circle);
|
||||
|
||||
// Create members
|
||||
let mut alice = Member::new(
|
||||
1,
|
||||
"Alice".to_string(),
|
||||
"Core contributor".to_string(),
|
||||
Role::Admin,
|
||||
);
|
||||
alice.add_email("alice@example.com".to_string());
|
||||
|
||||
let mut bob = Member::new(
|
||||
2,
|
||||
"Bob".to_string(),
|
||||
"Community member".to_string(),
|
||||
Role::Member,
|
||||
);
|
||||
bob.add_email("bob@example.com".to_string());
|
||||
|
||||
println!("Created members: {:?}, {:?}", alice, bob);
|
||||
|
||||
// Create a domain name
|
||||
let mut domain = Name::new(
|
||||
1,
|
||||
"threefold.io".to_string(),
|
||||
"ThreeFold main domain".to_string(),
|
||||
);
|
||||
|
||||
let record = Record {
|
||||
name: "www".to_string(),
|
||||
text: "ThreeFold Website".to_string(),
|
||||
category: RecordType::A,
|
||||
addr: vec!["192.168.1.1".to_string()],
|
||||
};
|
||||
|
||||
domain.add_record(record);
|
||||
domain.add_admin("alice_pubkey".to_string());
|
||||
|
||||
println!("Created domain: {:?}", domain);
|
||||
|
||||
// Create wallets
|
||||
let mut alice_wallet = Wallet::new(
|
||||
1,
|
||||
"Alice's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GALICE...".to_string(),
|
||||
);
|
||||
|
||||
alice_wallet.set_asset("TFT".to_string(), 1000.0);
|
||||
alice_wallet.set_asset("XLM".to_string(), 100.0);
|
||||
|
||||
let mut bob_wallet = Wallet::new(
|
||||
2,
|
||||
"Bob's TFT Wallet".to_string(),
|
||||
"Main TFT wallet".to_string(),
|
||||
"Stellar".to_string(),
|
||||
"GBOB...".to_string(),
|
||||
);
|
||||
|
||||
bob_wallet.set_asset("TFT".to_string(), 500.0);
|
||||
|
||||
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
|
||||
|
||||
// Link wallets to members
|
||||
alice.link_wallet(alice_wallet.id);
|
||||
bob.link_wallet(bob_wallet.id);
|
||||
|
||||
println!("Linked wallets to members");
|
||||
|
||||
// Demonstrate wallet operations
|
||||
println!("\nDemonstrating wallet operations:");
|
||||
|
||||
println!("Alice's wallet before transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet before transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
// Simulate a transfer of 100 TFT from Alice to Bob
|
||||
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
|
||||
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
|
||||
|
||||
println!("Alice's wallet after transfer: {:?}", alice_wallet);
|
||||
println!("Alice's wallet total value: {}", alice_wallet.total_value());
|
||||
|
||||
println!("Bob's wallet after transfer: {:?}", bob_wallet);
|
||||
println!("Bob's wallet total value: {}", bob_wallet.total_value());
|
||||
|
||||
println!("\nCircle standalone demo completed successfully!");
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use herodb::db::{DB, DBBuilder, Model};
|
||||
use herodb::models::biz::{Product, ProductBuilder, ProductType, ProductStatus, Currency, CurrencyBuilder};
|
||||
use chrono::Utc;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("OurDB Backend Example");
|
||||
println!("====================\n");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = std::env::temp_dir().join("herodb_ourdb_example");
|
||||
std::fs::create_dir_all(&db_path)?;
|
||||
|
||||
println!("Creating database at: {}", db_path.display());
|
||||
|
||||
// Create a new database with Product model registered
|
||||
let db = DBBuilder::new(db_path.clone())
|
||||
.register_model::<Product>()
|
||||
.build()?;
|
||||
|
||||
println!("Database created successfully");
|
||||
|
||||
// Create a currency for pricing
|
||||
let usd = CurrencyBuilder::new()
|
||||
.id(1) // Add an ID for the currency
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to create currency");
|
||||
|
||||
// Create a product
|
||||
let product = ProductBuilder::new()
|
||||
.id(1) // We're setting an ID manually for this example
|
||||
.name("Test Product")
|
||||
.description("A test product for our OurDB example")
|
||||
.price(usd)
|
||||
.type_(ProductType::Product)
|
||||
.category("Test")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(365)
|
||||
.build()
|
||||
.expect("Failed to create product");
|
||||
|
||||
println!("\nCreated product: {}", product.name);
|
||||
println!("Product ID: {}", product.get_id());
|
||||
|
||||
// Insert the product into the database
|
||||
db.set(&product)?;
|
||||
println!("Product saved to database");
|
||||
|
||||
// Retrieve the product from the database
|
||||
let retrieved_product = db.get::<Product>(product.get_id())?;
|
||||
println!("\nRetrieved product from database:");
|
||||
println!(" Name: {}", retrieved_product.name);
|
||||
println!(" Description: {}", retrieved_product.description);
|
||||
println!(" Price: ${} {}", retrieved_product.price.amount, retrieved_product.price.currency_code);
|
||||
|
||||
// Create a product with auto-incremented ID
|
||||
// For this to work, we would need to modify the Product model to support auto-incremented IDs
|
||||
// This is just a conceptual example
|
||||
println!("\nDemonstrating auto-incremented IDs:");
|
||||
println!("(Note: This would require additional implementation in the Product model)");
|
||||
|
||||
// Delete the product
|
||||
db.delete::<Product>(product.get_id())?;
|
||||
println!("\nProduct deleted from database");
|
||||
|
||||
// Try to retrieve the deleted product (should fail)
|
||||
match db.get::<Product>(product.get_id()) {
|
||||
Ok(_) => println!("Product still exists (unexpected)"),
|
||||
Err(e) => println!("Verified deletion: {}", e),
|
||||
}
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
|
||||
// Clean up
|
||||
std::fs::remove_dir_all(&db_path)?;
|
||||
println!("Cleaned up database directory");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
use herodb::db::{DB, DBBuilder, Model, IndexKey};
|
||||
use herodb::models::biz::Customer;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("TST Index Example");
|
||||
println!("================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/tst_index_example");
|
||||
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 the Customer model registered
|
||||
let db = DBBuilder::new(&db_path)
|
||||
.register_model::<Customer>()
|
||||
.build()?;
|
||||
|
||||
// Create some customers
|
||||
let customer1 = Customer::new(
|
||||
1,
|
||||
"John Doe".to_string(),
|
||||
"A regular customer".to_string(),
|
||||
"pk123456".to_string(),
|
||||
);
|
||||
|
||||
let customer2 = Customer::new(
|
||||
2,
|
||||
"Jane Smith".to_string(),
|
||||
"A VIP customer".to_string(),
|
||||
"pk789012".to_string(),
|
||||
);
|
||||
|
||||
let customer3 = Customer::new(
|
||||
3,
|
||||
"John Smith".to_string(),
|
||||
"Another customer".to_string(),
|
||||
"pk345678".to_string(),
|
||||
);
|
||||
|
||||
// Insert the customers
|
||||
db.set(&customer1)?;
|
||||
db.set(&customer2)?;
|
||||
db.set(&customer3)?;
|
||||
|
||||
println!("\nCustomers created:");
|
||||
println!("1. {} ({})", customer1.name, customer1.pubkey);
|
||||
println!("2. {} ({})", customer2.name, customer2.pubkey);
|
||||
println!("3. {} ({})", customer3.name, customer3.pubkey);
|
||||
|
||||
// List all customers
|
||||
println!("\nListing all customers:");
|
||||
let customers = db.list::<Customer>()?;
|
||||
for customer in &customers {
|
||||
println!("- {} (ID: {})", customer.name, customer.id);
|
||||
}
|
||||
println!("Total: {} customers", customers.len());
|
||||
|
||||
// Find customers by name index
|
||||
println!("\nFinding customers by name 'John':");
|
||||
let john_customers = db.find_by_index_prefix::<Customer>("name", "John")?;
|
||||
for customer in &john_customers {
|
||||
println!("- {} (ID: {})", customer.name, customer.id);
|
||||
}
|
||||
println!("Total: {} customers", john_customers.len());
|
||||
|
||||
// Find customers by pubkey index
|
||||
println!("\nFinding customer by pubkey 'pk789012':");
|
||||
let pubkey_customers = db.find_by_index::<Customer>("pubkey", "pk789012")?;
|
||||
for customer in &pubkey_customers {
|
||||
println!("- {} (ID: {})", customer.name, customer.id);
|
||||
}
|
||||
println!("Total: {} customers", pubkey_customers.len());
|
||||
|
||||
// Delete a customer
|
||||
println!("\nDeleting customer with ID 2");
|
||||
db.delete::<Customer>(2)?;
|
||||
|
||||
// List all customers again
|
||||
println!("\nListing all customers after deletion:");
|
||||
let customers = db.list::<Customer>()?;
|
||||
for customer in &customers {
|
||||
println!("- {} (ID: {})", customer.name, customer.id);
|
||||
}
|
||||
println!("Total: {} customers", customers.len());
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,326 +0,0 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use herodb::db::{DBBuilder, DB, Model};
|
||||
use herodb::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 std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
/// 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() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Business Models Example");
|
||||
println!("=======================\n");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample_biz");
|
||||
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 business models registered
|
||||
let db = DBBuilder::new(&db_path)
|
||||
.register_model::<Customer>()
|
||||
.register_model::<Product>()
|
||||
.register_model::<Sale>()
|
||||
.register_model::<Invoice>()
|
||||
.build()?;
|
||||
|
||||
// Create a customer
|
||||
let customer = create_customer();
|
||||
println!("Created customer: {}", customer.name);
|
||||
db.set(&customer)?;
|
||||
|
||||
// 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);
|
||||
|
||||
// Store products in the database
|
||||
db.set(&standard_node)?;
|
||||
db.set(&premium_node)?;
|
||||
|
||||
// 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.get_id());
|
||||
println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code);
|
||||
db.set(&sale)?;
|
||||
|
||||
// Generate an invoice
|
||||
println!("\nGenerating invoice:");
|
||||
let invoice = create_invoice(&customer, &sale);
|
||||
println!(" - Invoice created with ID: {}", invoice.get_id());
|
||||
println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code);
|
||||
println!(" - Due date: {}", invoice.due_date);
|
||||
println!(" - Status: {:?}", invoice.status);
|
||||
db.set(&invoice)?;
|
||||
|
||||
// Simulate payment
|
||||
println!("\nSimulating payment:");
|
||||
let mut invoice_copy = invoice.clone();
|
||||
process_payment(&mut invoice_copy);
|
||||
println!(" - Payment processed");
|
||||
println!(" - New balance due: ${} {}", invoice_copy.balance_due.amount, invoice_copy.balance_due.currency_code);
|
||||
println!(" - Payment status: {:?}", invoice_copy.payment_status);
|
||||
println!(" - Invoice status: {:?}", invoice_copy.status);
|
||||
db.set(&invoice_copy)?;
|
||||
|
||||
// Retrieve data from the database to verify persistence
|
||||
println!("\nRetrieving data from database:");
|
||||
|
||||
// Retrieve customer
|
||||
let retrieved_customer = db.get::<Customer>(customer.get_id())?;
|
||||
println!("Retrieved customer: {} (ID: {})", retrieved_customer.name, retrieved_customer.get_id());
|
||||
|
||||
// Retrieve product
|
||||
let retrieved_product = db.get::<Product>(premium_node.get_id())?;
|
||||
println!("Retrieved product: {} (ID: {})", retrieved_product.name, retrieved_product.get_id());
|
||||
|
||||
// Retrieve sale
|
||||
let retrieved_sale = db.get::<Sale>(sale.get_id())?;
|
||||
println!("Retrieved sale: ID {} with {} items", retrieved_sale.get_id(), retrieved_sale.items.len());
|
||||
|
||||
// Retrieve invoice
|
||||
let retrieved_invoice = db.get::<Invoice>(invoice.get_id())?;
|
||||
println!("Retrieved invoice: ID {} with status {:?}", retrieved_invoice.get_id(), retrieved_invoice.status);
|
||||
|
||||
println!("\nBusiness transaction completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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 currencies for pricing
|
||||
let standard_price = CurrencyBuilder::new()
|
||||
.id(100)
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to create currency");
|
||||
|
||||
let premium_price = CurrencyBuilder::new()
|
||||
.id(101)
|
||||
.amount(199.99)
|
||||
.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(standard_price)
|
||||
.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(premium_price)
|
||||
.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(Some(product.get_id()))
|
||||
.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.get_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.get_id())
|
||||
.build()
|
||||
.expect("Failed to create invoice item");
|
||||
|
||||
// Create the invoice
|
||||
let invoice = InvoiceBuilder::new()
|
||||
.id(1)
|
||||
.customer_id(customer.get_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(invoice: &mut 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);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Business example for HeroDB
|
||||
//!
|
||||
//! This module demonstrates business models in action,
|
||||
//! including products, sales, invoices, and payments.
|
||||
|
||||
// Include the main module directly
|
||||
pub mod main;
|
||||
@@ -1,368 +0,0 @@
|
||||
use chrono::{Utc, Duration};
|
||||
use herodb::db::{DBBuilder, DB};
|
||||
use herodb::models::gov::{
|
||||
Company, CompanyStatus, BusinessType,
|
||||
Shareholder, ShareholderType,
|
||||
Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus,
|
||||
User,
|
||||
Vote, VoteOption, Ballot, VoteStatus,
|
||||
Resolution, ResolutionStatus, Approval,
|
||||
Committee, CommitteeRole, CommitteeMember
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example: Gov Module");
|
||||
println!("============================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample_gov");
|
||||
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 governance models registered
|
||||
let db = DBBuilder::new(&db_path)
|
||||
.register_model::<Company>()
|
||||
.register_model::<Shareholder>()
|
||||
.register_model::<Meeting>()
|
||||
.register_model::<User>()
|
||||
.register_model::<Vote>()
|
||||
.register_model::<Resolution>()
|
||||
.register_model::<Committee>()
|
||||
.build()?;
|
||||
|
||||
println!("\n1. Creating a Company");
|
||||
println!("-------------------");
|
||||
|
||||
// Create a company
|
||||
let company = Company::new(
|
||||
1,
|
||||
"Acme Corporation".to_string(),
|
||||
"ACM123456".to_string(),
|
||||
Utc::now(),
|
||||
"December 31".to_string(),
|
||||
"info@acmecorp.com".to_string(),
|
||||
"+1-555-123-4567".to_string(),
|
||||
"https://acmecorp.com".to_string(),
|
||||
"123 Main St, Anytown, USA".to_string(),
|
||||
BusinessType::new(BusinessType::COOP.to_string())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Warning: {}", e);
|
||||
BusinessType::new_unchecked(BusinessType::COOP.to_string())
|
||||
}),
|
||||
"Technology".to_string(),
|
||||
"A leading technology company".to_string(),
|
||||
CompanyStatus::Active,
|
||||
);
|
||||
|
||||
// Insert the company
|
||||
db.set(&company)?;
|
||||
println!("Company created: {} (ID: {})", company.name, company.id);
|
||||
println!("Status: {:?}, Business Type: {}", company.status, company.business_type.as_str());
|
||||
|
||||
println!("\n2. Creating Users");
|
||||
println!("---------------");
|
||||
|
||||
// Create users
|
||||
let user1 = User::new(
|
||||
1,
|
||||
"John Doe".to_string(),
|
||||
"john.doe@acmecorp.com".to_string(),
|
||||
"password123".to_string(), // In a real app, this would be hashed
|
||||
"Acme Corporation".to_string(),
|
||||
"CEO".to_string(),
|
||||
);
|
||||
|
||||
let user2 = User::new(
|
||||
2,
|
||||
"Jane Smith".to_string(),
|
||||
"jane.smith@acmecorp.com".to_string(),
|
||||
"password456".to_string(), // In a real app, this would be hashed
|
||||
"Acme Corporation".to_string(),
|
||||
"CFO".to_string(),
|
||||
);
|
||||
|
||||
let user3 = User::new(
|
||||
3,
|
||||
"Bob Johnson".to_string(),
|
||||
"bob.johnson@acmecorp.com".to_string(),
|
||||
"password789".to_string(), // In a real app, this would be hashed
|
||||
"Acme Corporation".to_string(),
|
||||
"CTO".to_string(),
|
||||
);
|
||||
|
||||
// Insert the users
|
||||
db.set(&user1)?;
|
||||
db.set(&user2)?;
|
||||
db.set(&user3)?;
|
||||
|
||||
println!("User created: {} ({})", user1.name, user1.role);
|
||||
println!("User created: {} ({})", user2.name, user2.role);
|
||||
println!("User created: {} ({})", user3.name, user3.role);
|
||||
|
||||
println!("\n3. Creating Shareholders");
|
||||
println!("----------------------");
|
||||
|
||||
// Create shareholders
|
||||
let mut shareholder1 = Shareholder::new(
|
||||
1,
|
||||
company.id,
|
||||
user1.id,
|
||||
user1.name.clone(),
|
||||
1000.0,
|
||||
40.0,
|
||||
ShareholderType::Individual,
|
||||
);
|
||||
|
||||
let mut shareholder2 = Shareholder::new(
|
||||
2,
|
||||
company.id,
|
||||
user2.id,
|
||||
user2.name.clone(),
|
||||
750.0,
|
||||
30.0,
|
||||
ShareholderType::Individual,
|
||||
);
|
||||
|
||||
let mut shareholder3 = Shareholder::new(
|
||||
3,
|
||||
company.id,
|
||||
user3.id,
|
||||
user3.name.clone(),
|
||||
750.0,
|
||||
30.0,
|
||||
ShareholderType::Individual,
|
||||
);
|
||||
|
||||
// Insert the shareholders
|
||||
db.set(&shareholder1)?;
|
||||
db.set(&shareholder2)?;
|
||||
db.set(&shareholder3)?;
|
||||
|
||||
println!("Shareholder created: {} ({} shares, {}%)",
|
||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||
println!("Shareholder created: {} ({} shares, {}%)",
|
||||
shareholder2.name, shareholder2.shares, shareholder2.percentage);
|
||||
println!("Shareholder created: {} ({} shares, {}%)",
|
||||
shareholder3.name, shareholder3.shares, shareholder3.percentage);
|
||||
|
||||
// Update shareholder shares
|
||||
shareholder1.update_shares(1100.0, 44.0);
|
||||
db.set(&shareholder1)?;
|
||||
println!("Updated shareholder: {} ({} shares, {}%)",
|
||||
shareholder1.name, shareholder1.shares, shareholder1.percentage);
|
||||
|
||||
println!("\n4. Creating a Meeting");
|
||||
println!("------------------");
|
||||
|
||||
// Create a meeting
|
||||
let mut meeting = Meeting::new(
|
||||
1,
|
||||
company.id,
|
||||
"Q2 Board Meeting".to_string(),
|
||||
Utc::now() + Duration::days(7), // Meeting in 7 days
|
||||
"Conference Room A".to_string(),
|
||||
"Quarterly board meeting to discuss financial results".to_string(),
|
||||
);
|
||||
|
||||
// Create attendees
|
||||
let attendee1 = Attendee::new(
|
||||
1,
|
||||
meeting.id,
|
||||
user1.id,
|
||||
user1.name.clone(),
|
||||
AttendeeRole::Coordinator,
|
||||
);
|
||||
|
||||
let attendee2 = Attendee::new(
|
||||
2,
|
||||
meeting.id,
|
||||
user2.id,
|
||||
user2.name.clone(),
|
||||
AttendeeRole::Member,
|
||||
);
|
||||
|
||||
let attendee3 = Attendee::new(
|
||||
3,
|
||||
meeting.id,
|
||||
user3.id,
|
||||
user3.name.clone(),
|
||||
AttendeeRole::Member,
|
||||
);
|
||||
|
||||
// Add attendees to the meeting
|
||||
meeting.add_attendee(attendee1);
|
||||
meeting.add_attendee(attendee2);
|
||||
meeting.add_attendee(attendee3);
|
||||
|
||||
// Insert the meeting
|
||||
db.set(&meeting)?;
|
||||
println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M"));
|
||||
println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len());
|
||||
|
||||
// Update attendee status
|
||||
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user2.id) {
|
||||
attendee.update_status(AttendeeStatus::Confirmed);
|
||||
}
|
||||
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) {
|
||||
attendee.update_status(AttendeeStatus::Confirmed);
|
||||
}
|
||||
db.set(&meeting)?;
|
||||
|
||||
// Get confirmed attendees
|
||||
let confirmed = meeting.confirmed_attendees();
|
||||
println!("Confirmed attendees: {}", confirmed.len());
|
||||
for attendee in confirmed {
|
||||
println!(" - {} ({})", attendee.name, match attendee.role {
|
||||
AttendeeRole::Coordinator => "Coordinator",
|
||||
AttendeeRole::Member => "Member",
|
||||
AttendeeRole::Secretary => "Secretary",
|
||||
AttendeeRole::Participant => "Participant",
|
||||
AttendeeRole::Advisor => "Advisor",
|
||||
AttendeeRole::Admin => "Admin",
|
||||
});
|
||||
}
|
||||
|
||||
println!("\n5. Creating a Resolution");
|
||||
println!("----------------------");
|
||||
|
||||
// Create a resolution
|
||||
let mut resolution = Resolution::new(
|
||||
1,
|
||||
company.id,
|
||||
"Approval of Q1 Financial Statements".to_string(),
|
||||
"Resolution to approve the Q1 financial statements".to_string(),
|
||||
"The Board of Directors hereby approves the financial statements for Q1 2025.".to_string(),
|
||||
user1.id, // Proposed by the CEO
|
||||
);
|
||||
|
||||
// Link the resolution to the meeting
|
||||
resolution.link_to_meeting(meeting.id);
|
||||
|
||||
// Insert the resolution
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status);
|
||||
|
||||
// Propose the resolution
|
||||
resolution.propose();
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d"));
|
||||
|
||||
// Add approvals
|
||||
resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string());
|
||||
resolution.add_approval(user2.id, user2.name.clone(), true, "Financials look good".to_string());
|
||||
resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string());
|
||||
db.set(&resolution)?;
|
||||
|
||||
// Check approval status
|
||||
println!("Approvals: {}, Rejections: {}",
|
||||
resolution.approval_count(),
|
||||
resolution.rejection_count());
|
||||
|
||||
// Approve the resolution
|
||||
resolution.approve();
|
||||
db.set(&resolution)?;
|
||||
println!("Resolution approved on {}",
|
||||
resolution.approved_at.unwrap().format("%Y-%m-%d"));
|
||||
|
||||
println!("\n6. Creating a Vote");
|
||||
println!("----------------");
|
||||
|
||||
// Create a vote
|
||||
let mut vote = Vote::new(
|
||||
1,
|
||||
company.id,
|
||||
"Vote on New Product Line".to_string(),
|
||||
"Vote to approve investment in new product line".to_string(),
|
||||
Utc::now(),
|
||||
Utc::now() + Duration::days(3), // Voting period of 3 days
|
||||
VoteStatus::Open,
|
||||
);
|
||||
|
||||
// Add voting options
|
||||
vote.add_option("Approve".to_string(), 0);
|
||||
vote.add_option("Reject".to_string(), 0);
|
||||
vote.add_option("Abstain".to_string(), 0);
|
||||
|
||||
// Insert the vote
|
||||
db.set(&vote)?;
|
||||
println!("Vote created: {} (Status: {:?})", vote.title, vote.status);
|
||||
println!("Voting period: {} to {}",
|
||||
vote.start_date.format("%Y-%m-%d"),
|
||||
vote.end_date.format("%Y-%m-%d"));
|
||||
|
||||
// Cast ballots
|
||||
vote.add_ballot(user1.id, 1, 1000); // User 1 votes "Approve" with 1000 shares
|
||||
vote.add_ballot(user2.id, 1, 750); // User 2 votes "Approve" with 750 shares
|
||||
vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares
|
||||
db.set(&vote)?;
|
||||
|
||||
// Check voting results
|
||||
println!("Voting results:");
|
||||
for option in &vote.options {
|
||||
println!(" - {}: {} votes", option.text, option.count);
|
||||
}
|
||||
|
||||
// Create a resolution for this vote
|
||||
let mut vote_resolution = Resolution::new(
|
||||
2,
|
||||
company.id,
|
||||
"Investment in New Product Line".to_string(),
|
||||
"Resolution to approve investment in new product line".to_string(),
|
||||
"The Board of Directors hereby approves an investment of $1,000,000 in the new product line.".to_string(),
|
||||
user1.id, // Proposed by the CEO
|
||||
);
|
||||
|
||||
// Link the resolution to the vote
|
||||
vote_resolution.link_to_vote(vote.id);
|
||||
vote_resolution.propose();
|
||||
db.set(&vote_resolution)?;
|
||||
println!("Created resolution linked to vote: {}", vote_resolution.title);
|
||||
|
||||
println!("\n7. Retrieving Related Objects");
|
||||
println!("---------------------------");
|
||||
|
||||
// Retrieve company and related objects
|
||||
let retrieved_company = db.get::<Company>(company.id)?;
|
||||
println!("Company: {} (ID: {})", retrieved_company.name, retrieved_company.id);
|
||||
|
||||
// Get resolutions for this company
|
||||
let company_resolutions = retrieved_company.get_resolutions(&db)?;
|
||||
println!("Company has {} resolutions:", company_resolutions.len());
|
||||
for res in company_resolutions {
|
||||
println!(" - {} (Status: {:?})", res.title, res.status);
|
||||
}
|
||||
|
||||
// Get meeting and its resolutions
|
||||
let retrieved_meeting = db.get::<Meeting>(meeting.id)?;
|
||||
println!("Meeting: {} ({})", retrieved_meeting.title, retrieved_meeting.date.format("%Y-%m-%d"));
|
||||
|
||||
let meeting_resolutions = retrieved_meeting.get_resolutions(&db)?;
|
||||
println!("Meeting has {} resolutions:", meeting_resolutions.len());
|
||||
for res in meeting_resolutions {
|
||||
println!(" - {} (Status: {:?})", res.title, res.status);
|
||||
}
|
||||
|
||||
// Get vote and its resolution
|
||||
let retrieved_vote = db.get::<Vote>(vote.id)?;
|
||||
println!("Vote: {} (Status: {:?})", retrieved_vote.title, retrieved_vote.status);
|
||||
|
||||
if let Ok(Some(vote_res)) = retrieved_vote.get_resolution(&db) {
|
||||
println!("Vote is linked to resolution: {}", vote_res.title);
|
||||
}
|
||||
|
||||
// Get resolution and its related objects
|
||||
let retrieved_resolution = db.get::<Resolution>(resolution.id)?;
|
||||
println!("Resolution: {} (Status: {:?})", retrieved_resolution.title, retrieved_resolution.status);
|
||||
|
||||
if let Ok(Some(res_meeting)) = retrieved_resolution.get_meeting(&db) {
|
||||
println!("Resolution is discussed in meeting: {}", res_meeting.title);
|
||||
}
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
use chrono::{Utc, Duration};
|
||||
use herodb::db::{DBBuilder, GetId};
|
||||
use herodb::models::mcc::{
|
||||
Calendar, Event,
|
||||
Email, Attachment, Envelope,
|
||||
Contact, Message
|
||||
};
|
||||
use herodb::models::circle::Circle;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example MCC: Mail, Calendar, Contacts with Group Support");
|
||||
println!("=======================================================");
|
||||
|
||||
// Create a temporary directory for the database
|
||||
let db_path = PathBuf::from("/tmp/dbexample_mcc");
|
||||
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_type::<Calendar>("calendar")
|
||||
.register_type::<Event>("event")
|
||||
.register_type::<Email>("email")
|
||||
.register_type::<Contact>("contact")
|
||||
.register_type::<Message>("message")
|
||||
.register_model::<Circle>() // Circle still uses the Model trait
|
||||
.build()?;
|
||||
|
||||
println!("\n1. Creating Circles (Groups)");
|
||||
println!("---------------------------");
|
||||
|
||||
// Create circles (groups)
|
||||
let work_circle = Circle::new(
|
||||
1,
|
||||
"Work".to_string(),
|
||||
"Work-related communications".to_string()
|
||||
);
|
||||
|
||||
let family_circle = Circle::new(
|
||||
2,
|
||||
"Family".to_string(),
|
||||
"Family communications".to_string()
|
||||
);
|
||||
|
||||
let friends_circle = Circle::new(
|
||||
3,
|
||||
"Friends".to_string(),
|
||||
"Friends communications".to_string()
|
||||
);
|
||||
|
||||
// Insert circles
|
||||
db.set::<Circle>(&work_circle)?;
|
||||
db.set::<Circle>(&family_circle)?;
|
||||
db.set::<Circle>(&friends_circle)?;
|
||||
|
||||
println!("Created circles:");
|
||||
println!(" - Circle #{}: {}", work_circle.id, work_circle.name);
|
||||
println!(" - Circle #{}: {}", family_circle.id, family_circle.name);
|
||||
println!(" - Circle #{}: {}", friends_circle.id, friends_circle.name);
|
||||
|
||||
println!("\n2. Creating Contacts with Group Support");
|
||||
println!("------------------------------------");
|
||||
|
||||
// Create contacts
|
||||
let mut john = Contact::new(
|
||||
1,
|
||||
"John".to_string(),
|
||||
"Doe".to_string(),
|
||||
"john.doe@example.com".to_string(),
|
||||
"work".to_string()
|
||||
);
|
||||
john.add_group(work_circle.id);
|
||||
|
||||
let mut alice = Contact::new(
|
||||
2,
|
||||
"Alice".to_string(),
|
||||
"Smith".to_string(),
|
||||
"alice.smith@example.com".to_string(),
|
||||
"family".to_string()
|
||||
);
|
||||
alice.add_group(family_circle.id);
|
||||
|
||||
let mut bob = Contact::new(
|
||||
3,
|
||||
"Bob".to_string(),
|
||||
"Johnson".to_string(),
|
||||
"bob.johnson@example.com".to_string(),
|
||||
"friends".to_string()
|
||||
);
|
||||
bob.add_group(friends_circle.id);
|
||||
bob.add_group(work_circle.id); // Bob is both a friend and a work contact
|
||||
|
||||
// Insert contacts
|
||||
db.set_any::<Contact>(&john, "contact")?;
|
||||
db.set_any::<Contact>(&alice, "contact")?;
|
||||
db.set_any::<Contact>(&bob, "contact")?;
|
||||
|
||||
println!("Created contacts:");
|
||||
println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups);
|
||||
println!(" - {}: {} (Groups: {:?})", alice.full_name(), alice.email, alice.groups);
|
||||
println!(" - {}: {} (Groups: {:?})", bob.full_name(), bob.email, bob.groups);
|
||||
|
||||
println!("\n3. Creating Calendars with Group Support");
|
||||
println!("-------------------------------------");
|
||||
|
||||
// Create calendars
|
||||
let mut work_calendar = Calendar::new(
|
||||
1,
|
||||
"Work Calendar".to_string(),
|
||||
"Work-related events".to_string()
|
||||
);
|
||||
work_calendar.add_group(work_circle.id);
|
||||
|
||||
let mut personal_calendar = Calendar::new(
|
||||
2,
|
||||
"Personal Calendar".to_string(),
|
||||
"Personal events".to_string()
|
||||
);
|
||||
personal_calendar.add_group(family_circle.id);
|
||||
personal_calendar.add_group(friends_circle.id);
|
||||
|
||||
// Insert calendars
|
||||
db.set_any::<Calendar>(&work_calendar, "calendar")?;
|
||||
db.set_any::<Calendar>(&personal_calendar, "calendar")?;
|
||||
|
||||
println!("Created calendars:");
|
||||
println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups);
|
||||
println!(" - {}: {} (Groups: {:?})", personal_calendar.id, personal_calendar.title, personal_calendar.groups);
|
||||
|
||||
println!("\n4. Creating Events with Group Support");
|
||||
println!("----------------------------------");
|
||||
|
||||
// Create events
|
||||
let now = Utc::now();
|
||||
let tomorrow = now + Duration::days(1);
|
||||
let next_week = now + Duration::days(7);
|
||||
|
||||
let mut work_meeting = Event::new(
|
||||
1,
|
||||
work_calendar.id,
|
||||
"Team Meeting".to_string(),
|
||||
"Weekly team sync".to_string(),
|
||||
"Conference Room A".to_string(),
|
||||
tomorrow,
|
||||
tomorrow + Duration::hours(1),
|
||||
"organizer@example.com".to_string()
|
||||
);
|
||||
work_meeting.add_group(work_circle.id);
|
||||
work_meeting.add_attendee(john.email.clone());
|
||||
work_meeting.add_attendee(bob.email.clone());
|
||||
|
||||
let mut family_dinner = Event::new(
|
||||
2,
|
||||
personal_calendar.id,
|
||||
"Family Dinner".to_string(),
|
||||
"Weekly family dinner".to_string(),
|
||||
"Home".to_string(),
|
||||
next_week,
|
||||
next_week + Duration::hours(2),
|
||||
"me@example.com".to_string()
|
||||
);
|
||||
family_dinner.add_group(family_circle.id);
|
||||
family_dinner.add_attendee(alice.email.clone());
|
||||
|
||||
// Insert events
|
||||
db.set_any::<Event>(&work_meeting, "event")?;
|
||||
db.set_any::<Event>(&family_dinner, "event")?;
|
||||
|
||||
println!("Created events:");
|
||||
println!(" - {}: {} on {} (Groups: {:?})",
|
||||
work_meeting.id,
|
||||
work_meeting.title,
|
||||
work_meeting.start_time.format("%Y-%m-%d %H:%M"),
|
||||
work_meeting.groups
|
||||
);
|
||||
println!(" - {}: {} on {} (Groups: {:?})",
|
||||
family_dinner.id,
|
||||
family_dinner.title,
|
||||
family_dinner.start_time.format("%Y-%m-%d %H:%M"),
|
||||
family_dinner.groups
|
||||
);
|
||||
|
||||
println!("\n5. Creating Emails with Group Support");
|
||||
println!("----------------------------------");
|
||||
|
||||
// Create emails
|
||||
let mut work_email = Email::new(
|
||||
1,
|
||||
101,
|
||||
1,
|
||||
"INBOX".to_string(),
|
||||
"Here are the meeting notes from yesterday's discussion.".to_string()
|
||||
);
|
||||
work_email.add_group(work_circle.id);
|
||||
|
||||
let work_attachment = Attachment {
|
||||
filename: "meeting_notes.pdf".to_string(),
|
||||
content_type: "application/pdf".to_string(),
|
||||
hash: "abc123def456".to_string(),
|
||||
size: 1024,
|
||||
};
|
||||
work_email.add_attachment(work_attachment);
|
||||
|
||||
let work_envelope = Envelope {
|
||||
date: now.timestamp(),
|
||||
subject: "Meeting Notes".to_string(),
|
||||
from: vec!["john.doe@example.com".to_string()],
|
||||
sender: vec!["john.doe@example.com".to_string()],
|
||||
reply_to: vec!["john.doe@example.com".to_string()],
|
||||
to: vec!["me@example.com".to_string()],
|
||||
cc: vec!["bob.johnson@example.com".to_string()],
|
||||
bcc: vec![],
|
||||
in_reply_to: "".to_string(),
|
||||
message_id: "msg123@example.com".to_string(),
|
||||
};
|
||||
work_email.set_envelope(work_envelope);
|
||||
|
||||
let mut family_email = Email::new(
|
||||
2,
|
||||
102,
|
||||
2,
|
||||
"INBOX".to_string(),
|
||||
"Looking forward to seeing you at dinner next week!".to_string()
|
||||
);
|
||||
family_email.add_group(family_circle.id);
|
||||
|
||||
let family_envelope = Envelope {
|
||||
date: now.timestamp(),
|
||||
subject: "Family Dinner".to_string(),
|
||||
from: vec!["alice.smith@example.com".to_string()],
|
||||
sender: vec!["alice.smith@example.com".to_string()],
|
||||
reply_to: vec!["alice.smith@example.com".to_string()],
|
||||
to: vec!["me@example.com".to_string()],
|
||||
cc: vec![],
|
||||
bcc: vec![],
|
||||
in_reply_to: "".to_string(),
|
||||
message_id: "msg456@example.com".to_string(),
|
||||
};
|
||||
family_email.set_envelope(family_envelope);
|
||||
|
||||
// Insert emails
|
||||
db.set_any::<Email>(&work_email, "email")?;
|
||||
db.set_any::<Email>(&family_email, "email")?;
|
||||
|
||||
println!("Created emails:");
|
||||
println!(" - From: {}, Subject: {} (Groups: {:?})",
|
||||
work_email.envelope.as_ref().unwrap().from[0],
|
||||
work_email.envelope.as_ref().unwrap().subject,
|
||||
work_email.groups
|
||||
);
|
||||
println!(" - From: {}, Subject: {} (Groups: {:?})",
|
||||
family_email.envelope.as_ref().unwrap().from[0],
|
||||
family_email.envelope.as_ref().unwrap().subject,
|
||||
family_email.groups
|
||||
);
|
||||
|
||||
println!("\n6. Creating Messages (Chat) with Group Support");
|
||||
println!("-----------------------------------------");
|
||||
|
||||
// Create messages
|
||||
let mut work_chat = Message::new(
|
||||
1,
|
||||
"thread_work_123".to_string(),
|
||||
"john.doe@example.com".to_string(),
|
||||
"Can we move the meeting to 3pm?".to_string()
|
||||
);
|
||||
work_chat.add_group(work_circle.id);
|
||||
work_chat.add_recipient("me@example.com".to_string());
|
||||
work_chat.add_recipient("bob.johnson@example.com".to_string());
|
||||
|
||||
let mut friends_chat = Message::new(
|
||||
2,
|
||||
"thread_friends_456".to_string(),
|
||||
"bob.johnson@example.com".to_string(),
|
||||
"Are we still on for the game this weekend?".to_string()
|
||||
);
|
||||
friends_chat.add_group(friends_circle.id);
|
||||
friends_chat.add_recipient("me@example.com".to_string());
|
||||
friends_chat.add_reaction("👍".to_string());
|
||||
|
||||
// Insert messages
|
||||
db.set_any::<Message>(&work_chat, "message")?;
|
||||
db.set_any::<Message>(&friends_chat, "message")?;
|
||||
|
||||
println!("Created messages:");
|
||||
println!(" - From: {}, Content: {} (Groups: {:?})",
|
||||
work_chat.sender_id,
|
||||
work_chat.content,
|
||||
work_chat.groups
|
||||
);
|
||||
println!(" - From: {}, Content: {} (Groups: {:?}, Reactions: {:?})",
|
||||
friends_chat.sender_id,
|
||||
friends_chat.content,
|
||||
friends_chat.groups,
|
||||
friends_chat.meta.reactions
|
||||
);
|
||||
|
||||
println!("\n7. Demonstrating Utility Methods");
|
||||
println!("------------------------------");
|
||||
|
||||
// Filter contacts by group
|
||||
println!("\nFiltering contacts by work group (ID: {}):", work_circle.id);
|
||||
let all_contacts = db.list_any::<Contact>()?;
|
||||
for contact in all_contacts {
|
||||
if contact.filter_by_groups(&[work_circle.id]) {
|
||||
println!(" - {} ({})", contact.full_name(), contact.email);
|
||||
}
|
||||
}
|
||||
|
||||
// Search emails by subject
|
||||
println!("\nSearching emails with subject containing 'Meeting':");
|
||||
let all_emails = db.list_any::<Email>()?;
|
||||
for email in all_emails {
|
||||
if email.search_by_subject("Meeting") {
|
||||
println!(" - Subject: {}, From: {}",
|
||||
email.envelope.as_ref().unwrap().subject,
|
||||
email.envelope.as_ref().unwrap().from[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get events for a calendar
|
||||
println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id);
|
||||
let all_events = db.list_any::<Event>()?;
|
||||
let work_events: Vec<Event> = all_events
|
||||
.into_iter()
|
||||
.filter(|event| event.calendar_id == work_calendar.id)
|
||||
.collect();
|
||||
for event in work_events {
|
||||
println!(" - {}: {} on {}",
|
||||
event.id,
|
||||
event.title,
|
||||
event.start_time.format("%Y-%m-%d %H:%M")
|
||||
);
|
||||
}
|
||||
|
||||
// Get attendee contacts for an event
|
||||
println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id);
|
||||
let all_contacts = db.list_any::<Contact>()?;
|
||||
let attendee_contacts: Vec<Contact> = all_contacts
|
||||
.into_iter()
|
||||
.filter(|contact| work_meeting.attendees.contains(&contact.email))
|
||||
.collect();
|
||||
for contact in attendee_contacts {
|
||||
println!(" - {} ({})", contact.full_name(), contact.email);
|
||||
}
|
||||
|
||||
// Convert email to message
|
||||
println!("\nConverting work email to message:");
|
||||
let email_to_message = work_email.to_message(3, "thread_converted_789".to_string());
|
||||
println!(" - Original Email Subject: {}", work_email.envelope.as_ref().unwrap().subject);
|
||||
println!(" - Converted Message Content: {}", email_to_message.content.split('\n').next().unwrap_or(""));
|
||||
println!(" - Converted Message Groups: {:?}", email_to_message.groups);
|
||||
|
||||
// Insert the converted message
|
||||
db.set_any::<Message>(&email_to_message, "message")?;
|
||||
|
||||
println!("\n8. Relationship Management");
|
||||
println!("------------------------");
|
||||
|
||||
// Get the calendar for an event
|
||||
println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id);
|
||||
let event_calendar = db.get_any::<Calendar>(family_dinner.calendar_id)?;
|
||||
println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description);
|
||||
|
||||
// Get events for a contact
|
||||
println!("\nGetting events where John Doe is an attendee:");
|
||||
let all_events = db.list_any::<Event>()?;
|
||||
let john_events: Vec<Event> = all_events
|
||||
.into_iter()
|
||||
.filter(|event| event.attendees.contains(&john.email))
|
||||
.collect();
|
||||
for event in john_events {
|
||||
println!(" - {}: {} on {}",
|
||||
event.id,
|
||||
event.title,
|
||||
event.start_time.format("%Y-%m-%d %H:%M")
|
||||
);
|
||||
}
|
||||
|
||||
// Get messages in the same thread
|
||||
println!("\nGetting all messages in the work chat thread:");
|
||||
let all_messages = db.list_any::<Message>()?;
|
||||
let thread_messages: Vec<Message> = all_messages
|
||||
.into_iter()
|
||||
.filter(|message| message.thread_id == work_chat.thread_id)
|
||||
.collect();
|
||||
for message in thread_messages {
|
||||
println!(" - From: {}, Content: {}", message.sender_id, message.content);
|
||||
}
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
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>> {
|
||||
// Note: This example has been modified to work with the current API
|
||||
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);
|
||||
|
||||
// Skip the Rhai engine part as it has compatibility issues
|
||||
println!("\nSkipping Rhai engine part due to compatibility issues");
|
||||
|
||||
// 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()?;
|
||||
|
||||
println!("\n1. Creating Products with Builder Pattern");
|
||||
println!("----------------------------------------");
|
||||
|
||||
// Create a currency using the builder
|
||||
let usd = CurrencyBuilder::new()
|
||||
.id(1) // Add the required ID
|
||||
.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()
|
||||
.id(2) // Add ID
|
||||
.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()
|
||||
.id(3) // Add ID
|
||||
.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(Some(1))
|
||||
.name("Standard Plan")
|
||||
.quantity(1)
|
||||
.unit_price(
|
||||
CurrencyBuilder::new()
|
||||
.id(4) // Add ID
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()?;
|
||||
|
||||
let sale = SaleBuilder::new()
|
||||
.id(1)
|
||||
.company_id(101)
|
||||
.customer_id(123)
|
||||
.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 customer #{} (${:.2})",
|
||||
sale.id, sale.customer_id, 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(())
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//! 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; // Commented out to avoid circular imports
|
||||
@@ -1,646 +0,0 @@
|
||||
use crate::db::error::{DbError, DbResult};
|
||||
use crate::db::model::{Model, IndexKey};
|
||||
use crate::db::store::{DbOperations, OurDbStore};
|
||||
use crate::db::generic_store::{GenericStore, GetId};
|
||||
use crate::db::tst_index::TSTIndexManager;
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
/// Represents a single database operation in a transaction
|
||||
#[derive(Debug, Clone)]
|
||||
enum DbOperation {
|
||||
Set {
|
||||
model_type: TypeId,
|
||||
serialized: Vec<u8>,
|
||||
model_prefix: String, // Add model prefix
|
||||
model_id: u32, // Add model ID
|
||||
},
|
||||
Delete {
|
||||
model_type: TypeId,
|
||||
id: u32,
|
||||
model_prefix: String, // Add model prefix
|
||||
},
|
||||
}
|
||||
|
||||
/// Transaction state for DB operations
|
||||
#[derive(Clone)]
|
||||
pub struct TransactionState {
|
||||
operations: Vec<DbOperation>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl TransactionState {
|
||||
/// Create a new transaction state
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
operations: Vec::new(),
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main DB manager that automatically handles all models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DB {
|
||||
db_path: PathBuf,
|
||||
|
||||
// Type map for generic operations
|
||||
type_map: HashMap<TypeId, Arc<RwLock<dyn DbOperations>>>,
|
||||
|
||||
// TST index manager
|
||||
tst_index: Arc<RwLock<TSTIndexManager>>,
|
||||
|
||||
// Transaction state
|
||||
transaction: Arc<RwLock<Option<TransactionState>>>,
|
||||
}
|
||||
|
||||
/// Builder for DB that allows registering models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DBBuilder {
|
||||
base_path: PathBuf,
|
||||
model_registrations: Vec<Arc<dyn ModelRegistration>>,
|
||||
}
|
||||
|
||||
/// Trait for model registration
|
||||
pub trait ModelRegistration: Send + Sync {
|
||||
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)>;
|
||||
}
|
||||
|
||||
/// Implementation of ModelRegistration for any Model type
|
||||
pub struct ModelRegistrar<T: Model> {
|
||||
phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Model> ModelRegistrar<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of ModelRegistration for any serializable type that implements GetId
|
||||
pub struct TypeRegistrar<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
|
||||
prefix: &'static str,
|
||||
phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> TypeRegistrar<T> {
|
||||
pub fn new(prefix: &'static str) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Model> ModelRegistration for ModelRegistrar<T> {
|
||||
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
|
||||
let store = OurDbStore::<T>::open(path.join(T::db_prefix()))?;
|
||||
Ok((TypeId::of::<T>(), Arc::new(RwLock::new(store)) as Arc<RwLock<dyn DbOperations>>))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> ModelRegistration for TypeRegistrar<T> {
|
||||
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
|
||||
let store = GenericStore::<T>::open(path, self.prefix)?;
|
||||
Ok((TypeId::of::<T>(), Arc::new(RwLock::new(store)) as Arc<RwLock<dyn DbOperations>>))
|
||||
}
|
||||
}
|
||||
|
||||
impl DBBuilder {
|
||||
/// Create a new DB builder
|
||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
|
||||
Self {
|
||||
base_path: base_path.into(),
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_path<P: Into<PathBuf>>(base_path: P) -> Self {
|
||||
Self {
|
||||
base_path: base_path.into(),
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a model type with the DB
|
||||
pub fn register_model<T: Model>(mut self) -> Self {
|
||||
self.model_registrations
|
||||
.push(Arc::new(ModelRegistrar::<T>::new()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Register any serializable type with the DB
|
||||
pub fn register_type<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||
mut self,
|
||||
prefix: &'static str
|
||||
) -> Self {
|
||||
self.model_registrations
|
||||
.push(Arc::new(TypeRegistrar::<T>::new(prefix)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the DB with the registered models
|
||||
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
|
||||
let base_path = self.base_path;
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Register all models
|
||||
let mut type_map: HashMap<TypeId, Arc<RwLock<dyn DbOperations>>> = HashMap::new();
|
||||
|
||||
for registration in self.model_registrations {
|
||||
let (type_id, store) = registration.register(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e))
|
||||
})?;
|
||||
type_map.insert(type_id, store);
|
||||
}
|
||||
|
||||
// Create the TST index manager
|
||||
let tst_index = TSTIndexManager::new(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not create TST index manager".to_string(), Box::new(e))
|
||||
})?;
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(DB {
|
||||
db_path: base_path,
|
||||
type_map,
|
||||
tst_index: Arc::new(RwLock::new(tst_index)),
|
||||
transaction,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DB {
|
||||
/// Create a new empty DB instance without any models
|
||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> DbResult<Self> {
|
||||
let base_path = base_path.into();
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
}
|
||||
|
||||
// Create the TST index manager
|
||||
let tst_index = TSTIndexManager::new(&base_path)?;
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(Self {
|
||||
db_path: base_path,
|
||||
type_map: HashMap::new(),
|
||||
tst_index: Arc::new(RwLock::new(tst_index)),
|
||||
transaction,
|
||||
})
|
||||
}
|
||||
|
||||
// Transaction-related methods
|
||||
|
||||
/// Begin a new transaction
|
||||
pub fn begin_transaction(&self) -> DbResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
if tx.is_some() {
|
||||
return Err(DbError::TransactionError(
|
||||
"Transaction already in progress".into(),
|
||||
));
|
||||
}
|
||||
*tx = Some(TransactionState::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a transaction is active
|
||||
pub fn has_active_transaction(&self) -> bool {
|
||||
let tx = self.transaction.read().unwrap();
|
||||
tx.is_some() && tx.as_ref().unwrap().active
|
||||
}
|
||||
|
||||
/// Apply a set operation with the serialized data - bypass transaction check
|
||||
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> DbResult<()> {
|
||||
// Get the database operations for this model type
|
||||
if let Some(db_ops) = self.type_map.get(&model_type) {
|
||||
// Just pass the raw serialized data to a special raw insert method
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
return db_ops_guard.insert_raw(serialized);
|
||||
}
|
||||
|
||||
Err(DbError::GeneralError(format!(
|
||||
"No DB registered for type ID {:?}",
|
||||
model_type
|
||||
)))
|
||||
}
|
||||
|
||||
/// Commit the current transaction, applying all operations
|
||||
pub fn commit_transaction(&self) -> DbResult<()> {
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
if let Some(tx_state) = tx_guard.take() {
|
||||
if !tx_state.active {
|
||||
return Err(DbError::TransactionError("Transaction not active".into()));
|
||||
}
|
||||
|
||||
// Create a backup of the transaction state in case we need to rollback
|
||||
let backup = tx_state.clone();
|
||||
|
||||
// Try to execute all operations
|
||||
let result = (|| {
|
||||
for op in tx_state.operations {
|
||||
match op {
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
model_prefix,
|
||||
model_id,
|
||||
} => {
|
||||
// Apply to OurDB
|
||||
self.apply_set_operation(model_type, &serialized)?;
|
||||
|
||||
// Apply to TST index (primary key only)
|
||||
// We can't easily get the index keys in the transaction commit
|
||||
// because we don't have the model type information at runtime
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
tst_index.set(&model_prefix, model_id, serialized.clone())?;
|
||||
}
|
||||
DbOperation::Delete {
|
||||
model_type,
|
||||
id,
|
||||
model_prefix,
|
||||
} => {
|
||||
// For delete operations, we can't get the index keys from the model
|
||||
// because it's already deleted. We'll just delete the primary key.
|
||||
|
||||
// Apply to OurDB
|
||||
let db_ops = self
|
||||
.type_map
|
||||
.get(&model_type)
|
||||
.ok_or_else(|| DbError::TypeError)?;
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
db_ops_guard.delete(id)?;
|
||||
|
||||
// Apply to TST index (primary key only)
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
tst_index.delete(&model_prefix, id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
// If any operation failed, restore the transaction state
|
||||
if result.is_err() {
|
||||
*tx_guard = Some(backup);
|
||||
return result;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DbError::TransactionError("No active transaction".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rollback the current transaction, discarding all operations
|
||||
pub fn rollback_transaction(&self) -> DbResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
if tx.is_none() {
|
||||
return Err(DbError::TransactionError("No active transaction".into()));
|
||||
}
|
||||
*tx = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the path to the database
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.db_path
|
||||
}
|
||||
|
||||
// Generic methods that work with any supported model type
|
||||
|
||||
/// Insert a model instance into its appropriate database based on type
|
||||
pub fn set<T: Model>(&self, model: &T) -> DbResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
// Serialize the model for later use
|
||||
let serialized = model.to_bytes()?;
|
||||
|
||||
// Get the index keys for this model
|
||||
let index_keys = model.db_keys();
|
||||
|
||||
// Record a Set operation in the transaction with prefix and ID
|
||||
tx_state.operations.push(DbOperation::Set {
|
||||
model_type: TypeId::of::<T>(),
|
||||
serialized,
|
||||
model_prefix: T::db_prefix().to_string(),
|
||||
model_id: model.get_id(),
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
db_ops_guard.insert(model)?;
|
||||
|
||||
// Also update the TST index with all index keys
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
let prefix = T::db_prefix();
|
||||
let id = model.get_id();
|
||||
let data = model.to_bytes()?;
|
||||
let index_keys = model.db_keys();
|
||||
tst_index.set_with_indexes(prefix, id, data, &index_keys)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert any serializable struct that implements GetId
|
||||
pub fn set_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||
&self,
|
||||
item: &T,
|
||||
prefix: &str
|
||||
) -> DbResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
// Serialize the item for later use
|
||||
let serialized = bincode::serialize(item).map_err(DbError::SerializationError)?;
|
||||
|
||||
// Record a Set operation in the transaction with prefix and ID
|
||||
tx_state.operations.push(DbOperation::Set {
|
||||
model_type: TypeId::of::<T>(),
|
||||
serialized,
|
||||
model_prefix: prefix.to_string(),
|
||||
model_id: item.get_id(),
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
// Serialize the item
|
||||
let data = bincode::serialize(item).map_err(DbError::SerializationError)?;
|
||||
|
||||
// Insert the raw data
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
db_ops_guard.insert_raw(&data)?;
|
||||
|
||||
// Also update the TST index (primary key only)
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
tst_index.set(prefix, item.get_id(), data)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the transaction state for the given type and id
|
||||
fn check_transaction<T: Model>(&self, id: u32) -> Option<Result<Option<T>, DbError>> {
|
||||
// Try to acquire a read lock on the transaction
|
||||
let tx_guard = self.transaction.read().unwrap();
|
||||
|
||||
if let Some(tx_state) = tx_guard.as_ref() {
|
||||
if !tx_state.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
let type_id = TypeId::of::<T>();
|
||||
|
||||
// Process operations in reverse order (last operation wins)
|
||||
for op in tx_state.operations.iter().rev() {
|
||||
match op {
|
||||
// First check if this ID has been deleted in the transaction
|
||||
DbOperation::Delete {
|
||||
model_type,
|
||||
id: op_id,
|
||||
model_prefix: _,
|
||||
} => {
|
||||
if *model_type == type_id && *op_id == id {
|
||||
// Return NotFound error for deleted records
|
||||
return Some(Err(DbError::NotFound(id)));
|
||||
}
|
||||
}
|
||||
// Then check if it has been set in the transaction
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
model_prefix: _,
|
||||
model_id,
|
||||
} => {
|
||||
if *model_type == type_id && *model_id == id {
|
||||
// Try to deserialize
|
||||
match T::from_bytes(serialized) {
|
||||
Ok(model) => {
|
||||
return Some(Ok(Some(model)));
|
||||
}
|
||||
Err(_) => continue, // Skip if deserialization fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in transaction (continue to database)
|
||||
None
|
||||
}
|
||||
|
||||
/// Get a model instance by its ID and type
|
||||
pub fn get<T: Model>(&self, id: u32) -> DbResult<T> {
|
||||
// First check if there's a pending value in the current transaction
|
||||
if let Some(tx_result) = self.check_transaction::<T>(id) {
|
||||
match tx_result {
|
||||
Ok(Some(model)) => return Ok(model),
|
||||
Ok(None) => return Err(DbError::NotFound(id)),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in transaction, get from database
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
let any_result = db_ops_guard.get(id)?;
|
||||
|
||||
// Try to downcast to T
|
||||
match any_result.downcast::<T>() {
|
||||
Ok(boxed_t) => Ok(*boxed_t),
|
||||
Err(_) => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get any serializable struct by its ID and type
|
||||
pub fn get_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||
&self,
|
||||
id: u32
|
||||
) -> DbResult<T> {
|
||||
// If not found in transaction, get from database
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
let any_result = db_ops_guard.get(id)?;
|
||||
|
||||
// Try to downcast to T
|
||||
match any_result.downcast::<T>() {
|
||||
Ok(boxed_t) => Ok(*boxed_t),
|
||||
Err(_) => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a model instance by its ID and type
|
||||
pub fn delete<T: Model>(&self, id: u32) -> DbResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
// Record a Delete operation in the transaction
|
||||
tx_state.operations.push(DbOperation::Delete {
|
||||
model_type: TypeId::of::<T>(),
|
||||
id,
|
||||
model_prefix: T::db_prefix().to_string(),
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
db_ops_guard.delete(id)?;
|
||||
|
||||
// Also delete from the TST index
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
tst_index.delete(T::db_prefix(), id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete any serializable struct by its ID and type
|
||||
pub fn delete_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||
&self,
|
||||
id: u32,
|
||||
prefix: &str
|
||||
) -> DbResult<()> {
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
db_ops_guard.delete(id)?;
|
||||
|
||||
// Also delete from the TST index
|
||||
let mut tst_index = self.tst_index.write().unwrap();
|
||||
tst_index.delete(prefix, id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all model instances of a given type
|
||||
pub fn list<T: Model>(&self) -> DbResult<Vec<T>> {
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let db_ops_guard = db_ops.read().unwrap();
|
||||
let any_result = db_ops_guard.list()?;
|
||||
|
||||
// Try to downcast to Vec<T>
|
||||
match any_result.downcast::<Vec<T>>() {
|
||||
Ok(boxed_vec) => Ok(*boxed_vec),
|
||||
Err(_) => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all instances of any serializable type
|
||||
pub fn list_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
|
||||
&self
|
||||
) -> DbResult<Vec<T>> {
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let db_ops_guard = db_ops.read().unwrap();
|
||||
let any_result = db_ops_guard.list()?;
|
||||
|
||||
// Try to downcast to Vec<T>
|
||||
match any_result.downcast::<Vec<T>>() {
|
||||
Ok(boxed_vec) => Ok(*boxed_vec),
|
||||
Err(_) => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the history of a model instance
|
||||
pub fn get_history<T: Model>(&self, id: u32, depth: u8) -> DbResult<Vec<T>> {
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
let mut db_ops_guard = db_ops.write().unwrap();
|
||||
let any_results = db_ops_guard.get_history(id, depth)?;
|
||||
|
||||
let mut results = Vec::with_capacity(any_results.len());
|
||||
for any_result in any_results {
|
||||
match any_result.downcast::<T>() {
|
||||
Ok(boxed_t) => results.push(*boxed_t),
|
||||
Err(_) => return Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
None => Err(DbError::TypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
use thiserror::Error;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Errors that can occur during database operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DbError {
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Serialization/Deserialization error: {0}")]
|
||||
SerializationError(#[from] bincode::Error),
|
||||
|
||||
#[error("Record not found for ID: {0}")]
|
||||
NotFound(u32),
|
||||
|
||||
#[error("Type mismatch during deserialization")]
|
||||
TypeError,
|
||||
|
||||
#[error("Transaction error: {0}")]
|
||||
TransactionError(String),
|
||||
|
||||
#[error("OurDB error: {0}")]
|
||||
OurDbError(#[from] ourdb::Error),
|
||||
|
||||
#[error("General database error: {0}")]
|
||||
GeneralError(String),
|
||||
}
|
||||
|
||||
/// Result type for DB operations
|
||||
pub type DbResult<T> = Result<T, DbError>;
|
||||
@@ -1,140 +0,0 @@
|
||||
use crate::db::error::{DbError, DbResult};
|
||||
use crate::db::store::DbOperations;
|
||||
use ourdb::{OurDB, OurDBConfig, OurDBSetArgs};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::any::Any;
|
||||
|
||||
// Trait for getting ID from any serializable type
|
||||
pub trait GetId {
|
||||
fn get_id(&self) -> u32;
|
||||
}
|
||||
|
||||
/// A store implementation for any serializable type using OurDB as the backend
|
||||
pub struct GenericStore<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
|
||||
db: OurDB,
|
||||
path: PathBuf,
|
||||
prefix: String,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> GenericStore<T> {
|
||||
/// Opens or creates an OurDB database at the specified path
|
||||
pub fn open<P: AsRef<Path>>(path: P, prefix: &str) -> DbResult<Self> {
|
||||
let path_buf = path.as_ref().to_path_buf();
|
||||
let db_path = path_buf.join(prefix);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&db_path).map_err(DbError::IoError)?;
|
||||
|
||||
let config = OurDBConfig {
|
||||
path: db_path.clone(),
|
||||
incremental_mode: true, // Always use incremental mode for auto IDs
|
||||
file_size: None, // Use default (500MB)
|
||||
keysize: None, // Use default (4 bytes)
|
||||
reset: None, // Don't reset existing database
|
||||
};
|
||||
|
||||
let db = OurDB::new(config).map_err(DbError::OurDbError)?;
|
||||
|
||||
Ok(Self {
|
||||
db,
|
||||
path: db_path,
|
||||
prefix: prefix.to_string(),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes an item to bytes
|
||||
fn serialize(item: &T) -> DbResult<Vec<u8>> {
|
||||
bincode::serialize(item).map_err(DbError::SerializationError)
|
||||
}
|
||||
|
||||
/// Deserializes bytes to an item
|
||||
fn deserialize(data: &[u8]) -> DbResult<T> {
|
||||
bincode::deserialize(data).map_err(DbError::SerializationError)
|
||||
}
|
||||
|
||||
/// Gets the raw bytes for an item by ID
|
||||
pub fn get_raw(&self, id: u32) -> DbResult<Vec<u8>> {
|
||||
self.db.get(id).map_err(DbError::OurDbError)
|
||||
}
|
||||
|
||||
/// Lists all raw items as bytes
|
||||
pub fn list_raw(&self) -> DbResult<Vec<Vec<u8>>> {
|
||||
let items = self.db.list().map_err(DbError::OurDbError)?;
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Get the prefix for this store
|
||||
pub fn prefix(&self) -> &str {
|
||||
&self.prefix
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> DbOperations for GenericStore<T> {
|
||||
fn delete(&mut self, id: u32) -> DbResult<()> {
|
||||
self.db.delete(id).map_err(DbError::OurDbError)
|
||||
}
|
||||
|
||||
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>> {
|
||||
let data = self.db.get(id).map_err(DbError::OurDbError)?;
|
||||
let item = Self::deserialize(&data)?;
|
||||
Ok(Box::new(item))
|
||||
}
|
||||
|
||||
fn list(&self) -> DbResult<Box<dyn Any>> {
|
||||
let items = self.db.list().map_err(DbError::OurDbError)?;
|
||||
let mut result = Vec::with_capacity(items.len());
|
||||
|
||||
for data in items {
|
||||
let item = Self::deserialize(&data)?;
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
fn insert(&mut self, model: &dyn Any) -> DbResult<()> {
|
||||
// Try to downcast to T
|
||||
if let Some(item) = model.downcast_ref::<T>() {
|
||||
let data = Self::serialize(item)?;
|
||||
let id = item.get_id();
|
||||
|
||||
let args = OurDBSetArgs {
|
||||
id: Some(id),
|
||||
data,
|
||||
};
|
||||
|
||||
self.db.set(args).map_err(DbError::OurDbError)
|
||||
} else {
|
||||
Err(DbError::TypeError)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()> {
|
||||
// Deserialize to get the ID
|
||||
let item = Self::deserialize(serialized)?;
|
||||
let id = item.get_id();
|
||||
|
||||
let args = OurDBSetArgs {
|
||||
id: Some(id),
|
||||
data: serialized.to_vec(),
|
||||
};
|
||||
|
||||
self.db.set(args).map_err(DbError::OurDbError)
|
||||
}
|
||||
|
||||
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>> {
|
||||
let history = self.db.get_history(id, depth).map_err(DbError::OurDbError)?;
|
||||
let mut result = Vec::with_capacity(history.len());
|
||||
|
||||
for data in history {
|
||||
let item = Self::deserialize(&data)?;
|
||||
result.push(Box::new(item));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Macros for implementing model methods
|
||||
|
||||
/// Macro to implement typed access methods on the DB struct for a given model
|
||||
#[macro_export]
|
||||
macro_rules! impl_model_methods {
|
||||
($model:ty, $singular:ident, $plural:ident) => {
|
||||
impl DB {
|
||||
paste::paste! {
|
||||
/// Insert a model instance into the database
|
||||
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
Ok(self.set(&item).map_err(|e| {
|
||||
rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e))
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Get a model instance by its ID
|
||||
pub fn [<get_ $singular>](&mut self, id: u32) -> DbResult<$model> {
|
||||
self.get::<$model>(id)
|
||||
}
|
||||
|
||||
/// Delete a model instance by its ID
|
||||
pub fn [<delete_ $singular>](&mut self, id: u32) -> DbResult<()> {
|
||||
self.delete::<$model>(id)
|
||||
}
|
||||
|
||||
/// List all model instances
|
||||
pub fn [<list_ $plural>](&mut self) -> DbResult<Vec<$model>> {
|
||||
self.list::<$model>()
|
||||
}
|
||||
|
||||
/// Get history of a model instance
|
||||
pub fn [<get_ $singular _history>](&mut self, id: u32, depth: u8) -> DbResult<Vec<$model>> {
|
||||
self.get_history::<$model>(id, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Export the error module
|
||||
pub mod error;
|
||||
pub use error::{DbError, DbResult};
|
||||
|
||||
// Export the model module
|
||||
pub mod model;
|
||||
pub use model::{Model, Storable, IndexKey, GetId};
|
||||
|
||||
// Export the store module
|
||||
pub mod store;
|
||||
pub use store::{DbOperations, OurDbStore};
|
||||
|
||||
// Export the generic store module
|
||||
pub mod generic_store;
|
||||
pub use generic_store::GenericStore;
|
||||
|
||||
// Export the db module
|
||||
pub mod db;
|
||||
pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar};
|
||||
|
||||
// Export the TST index module
|
||||
pub mod tst_index;
|
||||
pub use tst_index::TSTIndexManager;
|
||||
|
||||
// Export macros for model methods
|
||||
pub mod macros;
|
||||
|
||||
// Export model-specific methods
|
||||
pub mod model_methods;
|
||||
|
||||
// Tests
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -1,96 +0,0 @@
|
||||
use crate::db::error::{DbError, DbResult};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Trait for models that can be serialized and deserialized
|
||||
pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||
/// Serializes the instance using bincode
|
||||
fn to_bytes(&self) -> DbResult<Vec<u8>> {
|
||||
bincode::serialize(self).map_err(DbError::SerializationError)
|
||||
}
|
||||
|
||||
/// Deserializes data from bytes into an instance
|
||||
fn from_bytes(data: &[u8]) -> DbResult<Self> {
|
||||
bincode::deserialize(data).map_err(DbError::SerializationError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an index key for a model
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexKey {
|
||||
/// The name of the index key
|
||||
pub name: &'static str,
|
||||
|
||||
/// The value of the index key for a specific model instance
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Trait identifying a model suitable for the database
|
||||
/// The 'static lifetime bound is required for type identification via Any
|
||||
pub trait Model: Storable + Debug + Clone + Send + Sync + 'static {
|
||||
/// Returns the unique ID for this model instance
|
||||
fn get_id(&self) -> u32;
|
||||
|
||||
/// Returns a prefix used for this model type in the database
|
||||
/// Helps to logically separate different model types
|
||||
fn db_prefix() -> &'static str;
|
||||
|
||||
/// Returns a list of index keys for this model instance
|
||||
/// These keys will be used to create additional indexes in the TST
|
||||
/// The default implementation returns an empty vector
|
||||
/// Override this method to provide custom indexes
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for adapting any serializable struct to work with the database
|
||||
/// This is a lighter-weight alternative to the Model trait
|
||||
pub trait ModelAdapter {
|
||||
/// Returns the unique ID for this model instance
|
||||
fn get_id(&self) -> u32;
|
||||
|
||||
/// Returns a prefix used for this model type in the database
|
||||
fn db_prefix() -> &'static str;
|
||||
|
||||
/// Returns a list of index keys for this model instance
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for getting ID from any serializable type
|
||||
pub trait GetId {
|
||||
/// Returns the unique ID for this instance
|
||||
fn get_id(&self) -> u32;
|
||||
}
|
||||
|
||||
/// Macro to automatically implement GetId for any struct with an id field of type u32
|
||||
#[macro_export]
|
||||
macro_rules! impl_get_id {
|
||||
($type:ty) => {
|
||||
impl GetId for $type {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper functions for serializing and deserializing any type
|
||||
pub mod serialization {
|
||||
use super::*;
|
||||
|
||||
/// Serialize any serializable type to bytes
|
||||
pub fn to_bytes<T: Serialize>(value: &T) -> DbResult<Vec<u8>> {
|
||||
bincode::serialize(value).map_err(DbError::SerializationError)
|
||||
}
|
||||
|
||||
/// Deserialize bytes to any deserializable type
|
||||
pub fn from_bytes<T: DeserializeOwned>(data: &[u8]) -> DbResult<T> {
|
||||
bincode::deserialize(data).map_err(DbError::SerializationError)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We don't provide a blanket implementation of Storable
|
||||
// Each model type must implement Storable explicitly
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::db::db::DB;
|
||||
use crate::db::model::Model;
|
||||
use crate::impl_model_methods;
|
||||
use crate::DbResult; // Add DbResult import
|
||||
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
|
||||
use crate::models::gov::{
|
||||
Company, Shareholder, Meeting, User, Vote, Resolution,
|
||||
Committee
|
||||
// ComplianceRequirement, ComplianceDocument, ComplianceAudit - These don't exist
|
||||
};
|
||||
use crate::models::circle::{Circle, Member, Name, Wallet}; // Remove Asset
|
||||
|
||||
// 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);
|
||||
|
||||
// Implement model-specific methods for Company
|
||||
impl_model_methods!(Company, company, companies);
|
||||
|
||||
// Implement model-specific methods for Shareholder
|
||||
impl_model_methods!(Shareholder, shareholder, shareholders);
|
||||
|
||||
// Implement model-specific methods for Meeting
|
||||
impl_model_methods!(Meeting, meeting, meetings);
|
||||
|
||||
// Implement model-specific methods for User
|
||||
impl_model_methods!(User, user, users);
|
||||
|
||||
// Implement model-specific methods for Vote
|
||||
impl_model_methods!(Vote, vote, votes);
|
||||
|
||||
// Implement model-specific methods for Resolution
|
||||
impl_model_methods!(Resolution, resolution, resolutions);
|
||||
|
||||
// Implement model-specific methods for Committee
|
||||
impl_model_methods!(Committee, committee, committees);
|
||||
|
||||
// These models don't exist, so comment them out
|
||||
// // Implement model-specific methods for ComplianceRequirement
|
||||
// impl_model_methods!(ComplianceRequirement, compliance_requirement, compliance_requirements);
|
||||
|
||||
// // Implement model-specific methods for ComplianceDocument
|
||||
// impl_model_methods!(ComplianceDocument, compliance_document, compliance_documents);
|
||||
|
||||
// // Implement model-specific methods for ComplianceAudit
|
||||
// impl_model_methods!(ComplianceAudit, compliance_audit, compliance_audits);
|
||||
|
||||
// Implement model-specific methods for Circle
|
||||
impl_model_methods!(Circle, circle, circles);
|
||||
|
||||
// Implement model-specific methods for Member
|
||||
impl_model_methods!(Member, member, members);
|
||||
|
||||
// Implement model-specific methods for Name
|
||||
impl_model_methods!(Name, name, names);
|
||||
|
||||
// Implement model-specific methods for Wallet
|
||||
impl_model_methods!(Wallet, wallet, wallets);
|
||||
@@ -1,156 +0,0 @@
|
||||
use crate::db::error::{DbError, DbResult};
|
||||
use crate::db::model::Model;
|
||||
use ourdb::{OurDB, OurDBConfig, OurDBSetArgs};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::any::Any;
|
||||
|
||||
/// Trait for type-erased database operations
|
||||
pub trait DbOperations: Send + Sync {
|
||||
fn delete(&mut self, id: u32) -> DbResult<()>;
|
||||
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>>;
|
||||
fn list(&self) -> DbResult<Box<dyn Any>>;
|
||||
fn insert(&mut self, model: &dyn Any) -> DbResult<()>;
|
||||
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()>;
|
||||
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>>;
|
||||
}
|
||||
|
||||
/// A store implementation using OurDB as the backend
|
||||
pub struct OurDbStore<T: Model> {
|
||||
db: OurDB,
|
||||
path: PathBuf,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Model> OurDbStore<T> {
|
||||
/// Opens or creates an OurDB database at the specified path
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> DbResult<Self> {
|
||||
let path_buf = path.as_ref().to_path_buf();
|
||||
let db_path = path_buf.join(T::db_prefix());
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&db_path).map_err(DbError::IoError)?;
|
||||
|
||||
let config = OurDBConfig {
|
||||
path: db_path.clone(),
|
||||
incremental_mode: true, // Always use incremental mode for auto IDs
|
||||
file_size: None, // Use default (500MB)
|
||||
keysize: None, // Use default (4 bytes)
|
||||
reset: None, // Don't reset existing database
|
||||
};
|
||||
|
||||
let db = OurDB::new(config).map_err(DbError::OurDbError)?;
|
||||
|
||||
Ok(Self {
|
||||
db,
|
||||
path: db_path,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts or updates a model instance in the database
|
||||
pub fn insert(&mut self, model: &T) -> DbResult<()> {
|
||||
// Use the new method name
|
||||
let data = model.to_bytes()?;
|
||||
|
||||
// Don't pass the ID when using incremental mode
|
||||
// OurDB will automatically assign an ID
|
||||
self.db.set(OurDBSetArgs {
|
||||
id: None,
|
||||
data: &data,
|
||||
}).map_err(DbError::OurDbError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a model instance by its ID
|
||||
pub fn get(&mut self, id: u32) -> DbResult<T> {
|
||||
let data = self.db.get(id).map_err(|e| {
|
||||
match e {
|
||||
ourdb::Error::NotFound(_) => DbError::NotFound(id),
|
||||
_ => DbError::OurDbError(e),
|
||||
}
|
||||
})?;
|
||||
|
||||
// Use the new method name
|
||||
T::from_bytes(&data)
|
||||
}
|
||||
|
||||
/// Deletes a model instance by its ID
|
||||
pub fn delete(&mut self, id: u32) -> DbResult<()> {
|
||||
self.db.delete(id).map_err(|e| {
|
||||
match e {
|
||||
ourdb::Error::NotFound(_) => DbError::NotFound(id),
|
||||
_ => DbError::OurDbError(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Lists all models of this type
|
||||
pub fn list(&self) -> DbResult<Vec<T>> {
|
||||
// OurDB doesn't have a built-in list function, so we need to implement it
|
||||
// This is a placeholder - in a real implementation, we would need to
|
||||
// maintain a list of all IDs for each model type
|
||||
Err(DbError::GeneralError("List operation not implemented yet".to_string()))
|
||||
}
|
||||
|
||||
/// Gets the history of a model by its ID
|
||||
pub fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<T>> {
|
||||
let history_data = self.db.get_history(id, depth).map_err(|e| {
|
||||
match e {
|
||||
ourdb::Error::NotFound(_) => DbError::NotFound(id),
|
||||
_ => DbError::OurDbError(e),
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut result = Vec::with_capacity(history_data.len());
|
||||
for data in history_data {
|
||||
result.push(T::from_bytes(&data)?);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Model> DbOperations for OurDbStore<T> {
|
||||
fn delete(&mut self, id: u32) -> DbResult<()> {
|
||||
self.delete(id)
|
||||
}
|
||||
|
||||
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>> {
|
||||
let result = self.get(id)?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
fn list(&self) -> DbResult<Box<dyn Any>> {
|
||||
// This doesn't require &mut self
|
||||
let result = self.list()?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
fn insert(&mut self, model: &dyn Any) -> DbResult<()> {
|
||||
// Downcast the Any to T
|
||||
if let Some(model_t) = model.downcast_ref::<T>() {
|
||||
self.insert(model_t)
|
||||
} else {
|
||||
Err(DbError::TypeError)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()> {
|
||||
// Deserialize the raw bytes to a model
|
||||
let model = T::from_bytes(serialized)?;
|
||||
self.insert(&model)
|
||||
}
|
||||
|
||||
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>> {
|
||||
let history = self.get_history(id, depth)?;
|
||||
let mut result = Vec::with_capacity(history.len());
|
||||
|
||||
for item in history {
|
||||
result.push(Box::new(item) as Box<dyn Any>);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
use super::*;
|
||||
use crate::db::model::Storable;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct TestModel {
|
||||
id: u32,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Storable for TestModel {}
|
||||
|
||||
impl Model for TestModel {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"test"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tst_integration() {
|
||||
// Create a temporary directory for the test
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
// Create a DB instance
|
||||
let mut db = DB::new(path).unwrap();
|
||||
db.register::<TestModel>().unwrap();
|
||||
|
||||
// Create some test models
|
||||
let model1 = TestModel { id: 1, name: "Test 1".to_string() };
|
||||
let model2 = TestModel { id: 2, name: "Test 2".to_string() };
|
||||
let model3 = TestModel { id: 3, name: "Test 3".to_string() };
|
||||
|
||||
// Insert the models
|
||||
db.set(&model1).unwrap();
|
||||
db.set(&model2).unwrap();
|
||||
db.set(&model3).unwrap();
|
||||
|
||||
// List all models
|
||||
let models = db.list::<TestModel>().unwrap();
|
||||
assert_eq!(models.len(), 3);
|
||||
|
||||
// Verify that all models are in the list
|
||||
assert!(models.contains(&model1));
|
||||
assert!(models.contains(&model2));
|
||||
assert!(models.contains(&model3));
|
||||
|
||||
// Delete a model
|
||||
db.delete::<TestModel>(2).unwrap();
|
||||
|
||||
// List again
|
||||
let models = db.list::<TestModel>().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(models.contains(&model1));
|
||||
assert!(models.contains(&model3));
|
||||
assert!(!models.contains(&model2));
|
||||
|
||||
// Test transaction with commit
|
||||
db.begin_transaction().unwrap();
|
||||
db.set(&model2).unwrap(); // Add back model2
|
||||
db.delete::<TestModel>(1).unwrap(); // Delete model1
|
||||
db.commit_transaction().unwrap();
|
||||
|
||||
// List again after transaction
|
||||
let models = db.list::<TestModel>().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(!models.contains(&model1));
|
||||
assert!(models.contains(&model2));
|
||||
assert!(models.contains(&model3));
|
||||
|
||||
// Test transaction with rollback
|
||||
db.begin_transaction().unwrap();
|
||||
db.delete::<TestModel>(3).unwrap(); // Delete model3
|
||||
db.rollback_transaction().unwrap();
|
||||
|
||||
// List again after rollback
|
||||
let models = db.list::<TestModel>().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(!models.contains(&model1));
|
||||
assert!(models.contains(&model2));
|
||||
assert!(models.contains(&model3));
|
||||
|
||||
// Test the synchronize_tst_index method
|
||||
// Since we can't directly access private fields, we'll just verify that
|
||||
// the method runs without errors
|
||||
db.synchronize_tst_index::<TestModel>().unwrap();
|
||||
|
||||
// Verify that our models are still accessible
|
||||
let models = db.list::<TestModel>().unwrap();
|
||||
assert_eq!(models.len(), 2);
|
||||
assert!(models.contains(&model2));
|
||||
assert!(models.contains(&model3));
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
use crate::db::error::{DbError, DbResult};
|
||||
use crate::db::model::IndexKey;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::HashMap;
|
||||
use tst::TST;
|
||||
|
||||
/// Manages TST-based indexes for model objects
|
||||
pub struct TSTIndexManager {
|
||||
/// Base path for TST databases
|
||||
base_path: PathBuf,
|
||||
|
||||
/// Map of model prefixes to their TST instances
|
||||
tst_instances: HashMap<String, TST>,
|
||||
}
|
||||
|
||||
impl TSTIndexManager {
|
||||
/// Creates a new TST index manager
|
||||
pub fn new<P: AsRef<Path>>(base_path: P) -> DbResult<Self> {
|
||||
let base_path = base_path.as_ref().to_path_buf();
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
std::fs::create_dir_all(&base_path).map_err(DbError::IoError)?;
|
||||
|
||||
Ok(Self {
|
||||
base_path,
|
||||
tst_instances: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets or creates a TST instance for a model prefix
|
||||
pub fn get_tst(&mut self, prefix: &str) -> DbResult<&mut TST> {
|
||||
if !self.tst_instances.contains_key(prefix) {
|
||||
// Create a new TST instance for this prefix
|
||||
let tst_path = self.base_path.join(format!("{}_tst", prefix));
|
||||
let tst_path_str = tst_path.to_string_lossy().to_string();
|
||||
|
||||
// Create the TST
|
||||
let tst = TST::new(&tst_path_str, false)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
// Insert it into the map
|
||||
self.tst_instances.insert(prefix.to_string(), tst);
|
||||
}
|
||||
|
||||
// Return a mutable reference to the TST
|
||||
Ok(self.tst_instances.get_mut(prefix).unwrap())
|
||||
}
|
||||
|
||||
/// Adds or updates an object in the TST index with primary key
|
||||
pub fn set(&mut self, prefix: &str, id: u32, data: Vec<u8>) -> DbResult<()> {
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Create the primary key in the format prefix_id
|
||||
let key = format!("{}_{}", prefix, id);
|
||||
|
||||
// Set the key-value pair in the TST
|
||||
tst.set(&key, data.clone())
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds or updates an object in the TST index with additional index keys
|
||||
pub fn set_with_indexes(&mut self, prefix: &str, id: u32, data: Vec<u8>, index_keys: &[IndexKey]) -> DbResult<()> {
|
||||
// First set the primary key
|
||||
self.set(prefix, id, data.clone())?;
|
||||
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Add additional index keys
|
||||
for index_key in index_keys {
|
||||
// Create the index key in the format prefix_indexname_value
|
||||
let key = format!("{}_{}_{}", prefix, index_key.name, index_key.value);
|
||||
|
||||
// Set the key-value pair in the TST
|
||||
// For index keys, we store the ID as the value
|
||||
let id_bytes = id.to_be_bytes().to_vec();
|
||||
tst.set(&key, id_bytes)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes an object from the TST index (primary key only)
|
||||
pub fn delete(&mut self, prefix: &str, id: u32) -> DbResult<()> {
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Create the key in the format prefix_id
|
||||
let key = format!("{}_{}", prefix, id);
|
||||
|
||||
// Delete the key from the TST
|
||||
tst.delete(&key)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes an object from the TST index including all index keys
|
||||
pub fn delete_with_indexes(&mut self, prefix: &str, id: u32, index_keys: &[IndexKey]) -> DbResult<()> {
|
||||
// First delete the primary key
|
||||
self.delete(prefix, id)?;
|
||||
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Delete additional index keys
|
||||
for index_key in index_keys {
|
||||
// Create the index key in the format prefix_indexname_value
|
||||
let key = format!("{}_{}_{}", prefix, index_key.name, index_key.value);
|
||||
|
||||
// Delete the key from the TST
|
||||
tst.delete(&key)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all objects with a given prefix (primary keys only)
|
||||
pub fn list(&mut self, prefix: &str) -> DbResult<Vec<(u32, Vec<u8>)>> {
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Get all keys with this prefix followed by an underscore
|
||||
let search_prefix = format!("{}_", prefix);
|
||||
let keys = tst.list(&search_prefix)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
// Get all values for these keys
|
||||
let mut result = Vec::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
// Check if this is a primary key (prefix_id) and not an index key (prefix_indexname_value)
|
||||
let parts: Vec<&str> = key.split('_').collect();
|
||||
if parts.len() != 2 {
|
||||
continue; // Skip index keys
|
||||
}
|
||||
|
||||
// Extract the ID from the key (format: prefix_id)
|
||||
let id_str = parts[1];
|
||||
let id = id_str.parse::<u32>().map_err(|_| {
|
||||
DbError::GeneralError(format!("Invalid ID in key: {}", key))
|
||||
})?;
|
||||
|
||||
// Get the value from the TST
|
||||
let data = tst.get(&key)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
result.push((id, data));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Finds objects by a specific index key
|
||||
pub fn find_by_index(&mut self, prefix: &str, index_name: &str, index_value: &str) -> DbResult<Vec<u32>> {
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Create the index key in the format prefix_indexname_value
|
||||
let key = format!("{}_{}_{}", prefix, index_name, index_value);
|
||||
|
||||
// Try to get the value from the TST
|
||||
match tst.get(&key) {
|
||||
Ok(id_bytes) => {
|
||||
// Convert the bytes to a u32 ID
|
||||
if id_bytes.len() == 4 {
|
||||
let mut bytes = [0u8; 4];
|
||||
bytes.copy_from_slice(&id_bytes[0..4]);
|
||||
let id = u32::from_be_bytes(bytes);
|
||||
Ok(vec![id])
|
||||
} else {
|
||||
Err(DbError::GeneralError(format!("Invalid ID bytes for key: {}", key)))
|
||||
}
|
||||
},
|
||||
Err(_) => Ok(Vec::new()), // No matches found
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds objects by a prefix of an index key
|
||||
pub fn find_by_index_prefix(&mut self, prefix: &str, index_name: &str, index_value_prefix: &str) -> DbResult<Vec<u32>> {
|
||||
// Get the TST for this prefix
|
||||
let tst = self.get_tst(prefix)?;
|
||||
|
||||
// Create the index key prefix in the format prefix_indexname_valueprefix
|
||||
let key_prefix = format!("{}_{}_{}", prefix, index_name, index_value_prefix);
|
||||
|
||||
// Get all keys with this prefix
|
||||
let keys = tst.list(&key_prefix)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
// Extract the IDs from the values
|
||||
let mut result = Vec::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
// Get the value from the TST
|
||||
let id_bytes = tst.get(&key)
|
||||
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
|
||||
|
||||
// Convert the bytes to a u32 ID
|
||||
if id_bytes.len() == 4 {
|
||||
let mut bytes = [0u8; 4];
|
||||
bytes.copy_from_slice(&id_bytes[0..4]);
|
||||
let id = u32::from_be_bytes(bytes);
|
||||
result.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_tst_index_manager() {
|
||||
// Create a temporary directory for the test
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
// Create a TST index manager
|
||||
let mut manager = TSTIndexManager::new(path).unwrap();
|
||||
|
||||
// Test setting values
|
||||
let data1 = vec![1, 2, 3];
|
||||
let data2 = vec![4, 5, 6];
|
||||
manager.set("test", 1, data1.clone()).unwrap();
|
||||
manager.set("test", 2, data2.clone()).unwrap();
|
||||
|
||||
// Test listing values
|
||||
let items = manager.list("test").unwrap();
|
||||
assert_eq!(items.len(), 2);
|
||||
|
||||
// Check that the values are correct
|
||||
let mut found_data1 = false;
|
||||
let mut found_data2 = false;
|
||||
for (id, data) in items {
|
||||
if id == 1 && data == data1 {
|
||||
found_data1 = true;
|
||||
} else if id == 2 && data == data2 {
|
||||
found_data2 = true;
|
||||
}
|
||||
}
|
||||
assert!(found_data1);
|
||||
assert!(found_data2);
|
||||
|
||||
// Test deleting a value
|
||||
manager.delete("test", 1).unwrap();
|
||||
|
||||
// Test listing again
|
||||
let items = manager.list("test").unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].0, 2);
|
||||
assert_eq!(items[0].1, data2);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error type for HeroDB operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
DbError(#[from] crate::db::error::DbError),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(#[from] bincode::Error),
|
||||
|
||||
#[error("OurDB error: {0}")]
|
||||
OurDbError(#[from] ourdb::Error),
|
||||
|
||||
#[error("General error: {0}")]
|
||||
GeneralError(String),
|
||||
}
|
||||
|
||||
/// Result type for HeroDB operations
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -1,143 +0,0 @@
|
||||
# HeroDB: ACL Layer Implementation
|
||||
|
||||
## Project Overview
|
||||
|
||||
Create a new module that implements an Access Control List (ACL) layer on top of the existing `ourdb` and `tst` databases. This module will manage permissions and access control for data stored in the database system.
|
||||
|
||||
call this module: acldb
|
||||
|
||||
implement in acldb
|
||||
|
||||
remark: there is no dependency on herodb
|
||||
|
||||
## Architecture
|
||||
|
||||
- The module will sit as a layer between client applications and the underlying `ourdb` & `tst` databases
|
||||
- ACLs are defined at the circle level and stored in a special topic called "acl"
|
||||
- Data in `ourdb` is stored at path: `~/hero/var/ourdb/$circleid/$topicid`
|
||||
- `tst` is used to create mappings between keys and IDs in `ourdb`
|
||||
|
||||
## ACL Structure
|
||||
|
||||
Each ACL contains:
|
||||
- A unique name (per circle)
|
||||
- A list of public keys with associated permissions
|
||||
- Rights are hierarchical: read → write → delete → execute → admin (each right includes all rights to its left)
|
||||
|
||||
## Core Methods
|
||||
|
||||
### ACL Management
|
||||
|
||||
#### aclupdate
|
||||
Updates or creates an ACL with specified permissions.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the ACL exists
|
||||
- `name`: Unique name for the ACL within the circle
|
||||
- `pubkeys`: Array of public keys to grant permissions to
|
||||
- `right`: Permission level (enum: read/write/delete/execute/admin)
|
||||
#### aclremove
|
||||
Removes specific public keys from an existing ACL.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the ACL exists
|
||||
- `name`: Name of the ACL to modify
|
||||
- `pubkeys`: Array of public keys to remove from the ACL
|
||||
|
||||
#### acldel
|
||||
Deletes an entire ACL.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the ACL exists
|
||||
- `name`: Name of the ACL to delete
|
||||
|
||||
### Data Operations
|
||||
|
||||
#### set
|
||||
Stores or updates data in the database with optional ACL protection.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the data belongs
|
||||
- `topic`: String identifier for the database category (e.g., "customer", "product")
|
||||
- `key`: Optional string key for the record
|
||||
- `id`: Optional numeric ID for direct access
|
||||
- `value`: Binary blob of data to store
|
||||
- `aclid`: ID of the ACL to protect this record (0 for public access)
|
||||
|
||||
**Behavior:**
|
||||
- If only `key` is provided, use `tst` to map the key to a new or existing ID
|
||||
- If `id` is specified or derived from an existing key, update the corresponding record
|
||||
- Returns the ID of the created/updated record
|
||||
|
||||
#### del
|
||||
Marks a record as deleted.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the data belongs
|
||||
- `topic`: String identifier for the database category
|
||||
- `id` or `key`: Identifier for the record to delete
|
||||
|
||||
**Behavior:**
|
||||
- Deletes the mapping in `tst` if a key was used
|
||||
- Marks the record as deleted in `ourdb` (not physically removed)
|
||||
|
||||
#### get
|
||||
Retrieves data from the database.
|
||||
|
||||
**Parameters:**
|
||||
- `callerpubkey`: Public key of the requesting user
|
||||
- `circleid`: ID of the circle where the data belongs
|
||||
- `topic`: String identifier for the database category
|
||||
- `id` or `key`: Identifier for the record to retrieve
|
||||
|
||||
**Returns:**
|
||||
- The binary data stored in the record if the caller has access
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### ACL Storage Format
|
||||
- ACLs are stored in a special topic named "acl" within each circle
|
||||
- Each ACL has a unique numeric ID within the circle
|
||||
|
||||
### Record ACL Protection
|
||||
- When a record uses ACL protection, the first 4 bytes of the stored data contain the ACL ID
|
||||
- A new constructor in `ourdb` should be created to handle ACL-protected records
|
||||
- Records with ACL ID of 0 are accessible to everyone
|
||||
|
||||
## RPC Interface
|
||||
|
||||
The module should expose its functionality through an RPC interface:
|
||||
|
||||
1. Client sends:
|
||||
- Method name (e.g., "del", "set", "get")
|
||||
- JSON-encoded arguments
|
||||
- Cryptographic signature of the JSON data
|
||||
|
||||
2. Server:
|
||||
- Verifies the signature is valid
|
||||
- Extracts the caller's public key from the signature
|
||||
- Checks permissions against applicable ACLs
|
||||
- Executes the requested operation if authorized
|
||||
- Returns appropriate response
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All operations must validate the caller has appropriate permissions
|
||||
- ACL changes should be logged for audit purposes
|
||||
- Consider implementing rate limiting to prevent abuse
|
||||
|
||||
## THE SERVER
|
||||
|
||||
- create actix webserver
|
||||
- make a router that handles the rpc interface
|
||||
- use openapi spec
|
||||
- embed swagger interface
|
||||
- implement a queuing mechanism, so internal we don't have to implement locks, but we do 1 request after the other per circle, so we know we never have conflicting changes in 1 circle
|
||||
- create a logger which gives us good overview of what happened when
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//! HeroDB: A database library built on top of ourdb with model support
|
||||
//!
|
||||
//! This library provides a simple interface for working with an ourdb-based database
|
||||
//! and includes support for defining and working with data models.
|
||||
|
||||
// Core modules
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
// Temporarily commented out due to compilation errors
|
||||
// pub mod rhaiengine;
|
||||
pub mod cmd;
|
||||
|
||||
// Re-exports
|
||||
pub use error::Error;
|
||||
pub use db::{DB, DBBuilder, Model, Storable, DbError, DbResult, GetId};
|
||||
|
||||
/// Re-export ourdb for advanced usage
|
||||
pub use ourdb;
|
||||
@@ -1,6 +0,0 @@
|
||||
// Export core module
|
||||
pub mod core;
|
||||
|
||||
// Export zaz module
|
||||
pub mod zaz;
|
||||
|
||||
@@ -1,530 +0,0 @@
|
||||
# Business Models
|
||||
|
||||
This directory contains the core business models used throughout the application for representing essential business objects like products, sales, and currency.
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Customer │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ │ │ │
|
||||
└─────────────┘ └─────────────┘ │ │ │ │
|
||||
▲ │ SaleItem │◄────┤ Sale │
|
||||
│ │ │ │ │
|
||||
┌─────┴──────────┐ │ │ │ │
|
||||
│ProductComponent│ └─────────────┘ └──────┬──────┘
|
||||
└────────────────┘ ▲ │
|
||||
/ │
|
||||
┌─────────────┐ ┌─────────────┐ / │
|
||||
│ Currency │◄────┤ Service │◄────────/ │
|
||||
└─────────────┘ └─────────────┘ │
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ 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
|
||||
- Contains SaleItems that can be linked to either Products or Services
|
||||
- **SaleItem**: Represents an item within a sale
|
||||
- Can be linked to either a Product or a Service (via product_id or service_id)
|
||||
- **Service**: Represents an ongoing service provided to a customer
|
||||
- Created from a Product template when the product type is Service
|
||||
- **Invoice**: Represents the billing document for a sale, with payment tracking
|
||||
- Created from a Sale object to handle billing and payment tracking
|
||||
|
||||
## Root Objects
|
||||
|
||||
- Root objects are the ones stored directly in the DB
|
||||
- Root Objects are:
|
||||
- Customer
|
||||
- Currency
|
||||
- Product
|
||||
- Sale
|
||||
- Service
|
||||
- Invoice
|
||||
|
||||
## Models
|
||||
|
||||
### Currency (Root Object)
|
||||
|
||||
Represents a monetary value with an amount and currency code.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: f64 - The monetary amount
|
||||
- `currency_code`: String - The currency code (e.g., "USD", "EUR")
|
||||
|
||||
**Builder:**
|
||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
||||
|
||||
### Customer (Root Object)
|
||||
|
||||
Represents a customer who can purchase products or services.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Customer name
|
||||
- `description`: String - Customer description
|
||||
- `pubkey`: String - Customer's public key
|
||||
- `contact_ids`: Vec<u32> - List of contact IDs
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Methods:**
|
||||
- `add_contact()` - Adds a contact ID to the customer
|
||||
- `remove_contact()` - Removes a contact ID from the customer
|
||||
|
||||
### Product
|
||||
|
||||
#### ProductType Enum
|
||||
Categorizes products:
|
||||
- `Product` - Physical product
|
||||
- `Service` - Non-physical service
|
||||
|
||||
#### ProductStatus Enum
|
||||
Tracks product availability:
|
||||
- `Available` - Product can be purchased
|
||||
- `Unavailable` - Product cannot be purchased
|
||||
|
||||
#### ProductComponent
|
||||
Represents a component part of a product.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Component name
|
||||
- `description`: String - Component description
|
||||
- `quantity`: i32 - Number of this component
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Builder:**
|
||||
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
|
||||
|
||||
#### Product (Root Object)
|
||||
Represents a product or service offered.
|
||||
|
||||
**Properties:**
|
||||
- `id`: i64 - Unique identifier
|
||||
- `name`: String - Product name
|
||||
- `description`: String - Product description
|
||||
- `price`: Currency - Product price
|
||||
- `type_`: ProductType - Product or Service
|
||||
- `category`: String - Product category
|
||||
- `status`: ProductStatus - Available or Unavailable
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `max_amount`: i64 - Maximum quantity available
|
||||
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||
- `active_till`: DateTime<Utc> - When product/service expires
|
||||
- `components`: Vec<ProductComponent> - List of product components
|
||||
- `is_template`: bool - Whether this is a template product (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_component()` - Adds a component to this product
|
||||
- `set_purchase_period()` - Updates purchase availability timeframe
|
||||
- `set_active_period()` - Updates active timeframe
|
||||
- `is_purchasable()` - Checks if product is available for purchase
|
||||
- `is_active()` - Checks if product is still active
|
||||
|
||||
**Builder:**
|
||||
- `ProductBuilder` - Provides a fluent interface for creating Product instances
|
||||
|
||||
**Database Implementation:**
|
||||
- Implements `Storable` trait for serialization
|
||||
- Implements `SledModel` trait with:
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `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
|
||||
|
||||
#### SaleStatus Enum
|
||||
Tracks the status of a sale:
|
||||
- `Pending` - Sale is in progress
|
||||
- `Completed` - Sale has been finalized
|
||||
- `Cancelled` - Sale has been cancelled
|
||||
|
||||
#### SaleItem
|
||||
Represents an item within a sale.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `sale_id`: u32 - Parent sale ID
|
||||
- `product_id`: Option<u32> - ID of the product sold (if this is a product sale)
|
||||
- `service_id`: Option<u32> - ID of the service sold (if this is a service sale)
|
||||
- `name`: String - Product/service name at time of sale
|
||||
- `description`: String - Detailed description of the item
|
||||
- `comments`: String - Additional notes or comments about the item
|
||||
- `quantity`: i32 - Number of items purchased
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price for this item before tax (calculated)
|
||||
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
- `tax_amount`: Currency - Calculated tax amount for this item
|
||||
- `active_till`: DateTime<Utc> - When item/service expires
|
||||
|
||||
**Methods:**
|
||||
- `total_with_tax()` - Returns the total amount including tax
|
||||
|
||||
**Builder:**
|
||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||
|
||||
#### Sale (Root Object)
|
||||
Represents a complete sale transaction.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `company_id`: u32 - ID of the company making the sale
|
||||
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
|
||||
- `buyer_name`: String - Name of the buyer
|
||||
- `buyer_email`: String - Email of the buyer
|
||||
- `subtotal_amount`: Currency - Total sale amount before tax
|
||||
- `tax_amount`: Currency - Total tax amount for the sale
|
||||
- `total_amount`: Currency - Total sale amount including tax
|
||||
- `status`: SaleStatus - Current sale status
|
||||
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
|
||||
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<SaleItem> - List of items in the sale
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the sale and updates totals
|
||||
- `update_status()` - Updates the status of the sale
|
||||
- `recalculate_totals()` - Recalculates all totals based on items
|
||||
- `create_service()` - Creates a service from this sale (to be added)
|
||||
|
||||
**Builder:**
|
||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||
|
||||
**Database Implementation:**
|
||||
- Implements `Storable` trait for serialization
|
||||
- Implements `SledModel` trait with:
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `db_prefix()` - Returns "sale" as the database prefix
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Currency
|
||||
|
||||
```rust
|
||||
let price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
```
|
||||
|
||||
### Creating a Product
|
||||
|
||||
```rust
|
||||
// Create a currency using the builder
|
||||
let price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
|
||||
// Create a component using the builder
|
||||
let component = ProductComponentBuilder::new()
|
||||
.id(1)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to build product component");
|
||||
|
||||
// Create a product using the builder
|
||||
let product = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Premium Service")
|
||||
.description("Our premium service offering")
|
||||
.price(price)
|
||||
.type_(ProductType::Service)
|
||||
.category("Services")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(30)
|
||||
.add_component(component)
|
||||
.build()
|
||||
.expect("Failed to build product");
|
||||
```
|
||||
|
||||
### Creating a Sale
|
||||
|
||||
```rust
|
||||
let now = Utc::now();
|
||||
|
||||
// Create a currency using the builder
|
||||
let unit_price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
|
||||
// Create a sale item using the builder
|
||||
let item = SaleItemBuilder::new()
|
||||
.id(1)
|
||||
.sale_id(1)
|
||||
.product_id(1)
|
||||
.name("Premium Service")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0) // 20% tax rate
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
|
||||
// Create a sale using the builder
|
||||
let mut 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(item)
|
||||
.build()
|
||||
.expect("Failed to build sale");
|
||||
|
||||
// Update the sale status
|
||||
sale.update_status(SaleStatus::Completed);
|
||||
|
||||
// The sale now contains:
|
||||
// - subtotal_amount: The sum of all items before tax
|
||||
// - tax_amount: The sum of all tax amounts
|
||||
// - total_amount: The total including tax
|
||||
```
|
||||
|
||||
### Relationship Between Sale and Invoice
|
||||
|
||||
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
|
||||
|
||||
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
|
||||
|
||||
```rust
|
||||
// Create an invoice item linked to a sale
|
||||
let invoice_item = InvoiceItemBuilder::new()
|
||||
.id(1)
|
||||
.invoice_id(1)
|
||||
.description("Premium Service")
|
||||
.amount(sale.total_amount.clone()) // Use the total amount from the sale
|
||||
.sale_id(sale.id) // Link to the sale
|
||||
.build()
|
||||
.expect("Failed to build invoice item");
|
||||
```
|
||||
|
||||
## Database Operations
|
||||
|
||||
The library provides model-specific convenience methods for common database operations:
|
||||
|
||||
```rust
|
||||
// Insert a product
|
||||
db.insert_product(&product).expect("Failed to insert product");
|
||||
|
||||
// Retrieve a product by ID
|
||||
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
|
||||
|
||||
// List all products
|
||||
let all_products = db.list_products().expect("Failed to list products");
|
||||
|
||||
// Delete a product
|
||||
db.delete_product(1).expect("Failed to delete product");
|
||||
```
|
||||
|
||||
These methods are available for all root objects:
|
||||
|
||||
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
||||
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
|
||||
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
|
||||
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
|
||||
|
||||
### Invoice (Root Object)
|
||||
|
||||
#### InvoiceStatus Enum
|
||||
Tracks the status of an invoice:
|
||||
- `Draft` - Invoice is in draft state
|
||||
- `Sent` - Invoice has been sent to the customer
|
||||
- `Paid` - Invoice has been paid
|
||||
- `Overdue` - Invoice is past due date
|
||||
- `Cancelled` - Invoice has been cancelled
|
||||
|
||||
#### PaymentStatus Enum
|
||||
Tracks the payment status of an invoice:
|
||||
- `Unpaid` - Invoice has not been paid
|
||||
- `PartiallyPaid` - Invoice has been partially paid
|
||||
- `Paid` - Invoice has been fully paid
|
||||
|
||||
#### Payment
|
||||
Represents a payment made against an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: Currency - Payment amount
|
||||
- `date`: DateTime<Utc> - Payment date
|
||||
- `method`: String - Payment method
|
||||
- `comment`: String - Payment comment
|
||||
|
||||
#### InvoiceItem
|
||||
Represents an item in an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `invoice_id`: u32 - Parent invoice ID
|
||||
- `description`: String - Item description
|
||||
- `amount`: Currency - Item amount
|
||||
- `service_id`: Option<u32> - ID of the service this item is for
|
||||
- `sale_id`: Option<u32> - ID of the sale this item is for
|
||||
|
||||
**Methods:**
|
||||
- `link_to_service()` - Links the invoice item to a service
|
||||
- `link_to_sale()` - Links the invoice item to a sale
|
||||
|
||||
#### Invoice
|
||||
Represents an invoice sent to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer being invoiced
|
||||
- `total_amount`: Currency - Total invoice amount
|
||||
- `balance_due`: Currency - Amount still due
|
||||
- `status`: InvoiceStatus - Current invoice status
|
||||
- `payment_status`: PaymentStatus - Current payment status
|
||||
- `issue_date`: DateTime<Utc> - When invoice was issued
|
||||
- `due_date`: DateTime<Utc> - When payment is due
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<InvoiceItem> - List of items in the invoice
|
||||
- `payments`: Vec<Payment> - List of payments made
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the invoice
|
||||
- `calculate_total()` - Calculates the total amount
|
||||
- `add_payment()` - Adds a payment to the invoice
|
||||
- `calculate_balance()` - Calculates the balance due
|
||||
- `update_payment_status()` - Updates the payment status
|
||||
- `update_status()` - Updates the status of the invoice
|
||||
- `is_overdue()` - Checks if the invoice is overdue
|
||||
- `check_if_overdue()` - Marks the invoice as overdue if past due date
|
||||
|
||||
### Relationships Between Models
|
||||
|
||||
#### Product/Service Templates and Instances
|
||||
|
||||
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
|
||||
|
||||
#### Sale to Service Relationship
|
||||
|
||||
A SaleItem can be directly linked to a Service via the `service_id` field. This allows for selling existing services or creating new services as part of a sale:
|
||||
|
||||
```rust
|
||||
// Create a SaleItem linked to a service
|
||||
let sale_item = SaleItemBuilder::new()
|
||||
.id(1)
|
||||
.sale_id(1)
|
||||
.service_id(Some(42)) // Link to service with ID 42
|
||||
.product_id(None) // No product link since this is a service
|
||||
.name("Premium Support")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
```
|
||||
|
||||
#### 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,293 +0,0 @@
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ContractStatus represents the status of a contract
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ContractStatus {
|
||||
Active,
|
||||
Expired,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
/// Contract represents a legal agreement between a customer and the business
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Contract {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub service_id: Option<u32>,
|
||||
pub sale_id: Option<u32>,
|
||||
pub terms: String,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub auto_renewal: bool,
|
||||
pub renewal_terms: String,
|
||||
pub status: ContractStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Contract {
|
||||
/// Create a new contract with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
terms: String,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
auto_renewal: bool,
|
||||
renewal_terms: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
terms,
|
||||
start_date,
|
||||
end_date,
|
||||
auto_renewal,
|
||||
renewal_terms,
|
||||
status: ContractStatus::Active,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Link the contract to a service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // A contract can only be linked to either a service or a sale
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link the contract to a sale
|
||||
pub fn link_to_sale(&mut self, sale_id: u32) {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // A contract can only be linked to either a service or a sale
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Check if the contract is currently active
|
||||
pub fn is_active(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
self.status == ContractStatus::Active &&
|
||||
now >= self.start_date &&
|
||||
now <= self.end_date
|
||||
}
|
||||
|
||||
/// Check if the contract has expired
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
now > self.end_date
|
||||
}
|
||||
|
||||
/// Update the contract status
|
||||
pub fn update_status(&mut self, status: ContractStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Renew the contract based on renewal terms
|
||||
pub fn renew(&mut self) -> Result<(), &'static str> {
|
||||
if !self.auto_renewal {
|
||||
return Err("Contract is not set for auto-renewal");
|
||||
}
|
||||
|
||||
if self.status != ContractStatus::Active {
|
||||
return Err("Cannot renew a non-active contract");
|
||||
}
|
||||
|
||||
// Calculate new dates based on the current end date
|
||||
let duration = self.end_date - self.start_date;
|
||||
self.start_date = self.end_date;
|
||||
self.end_date = self.end_date + duration;
|
||||
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Contract
|
||||
pub struct ContractBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
terms: Option<String>,
|
||||
start_date: Option<DateTime<Utc>>,
|
||||
end_date: Option<DateTime<Utc>>,
|
||||
auto_renewal: Option<bool>,
|
||||
renewal_terms: Option<String>,
|
||||
status: Option<ContractStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ContractBuilder {
|
||||
/// Create a new ContractBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
terms: None,
|
||||
start_date: None,
|
||||
end_date: None,
|
||||
auto_renewal: None,
|
||||
renewal_terms: None,
|
||||
status: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // A contract can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // A contract can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the terms
|
||||
pub fn terms<S: Into<String>>(mut self, terms: S) -> Self {
|
||||
self.terms = Some(terms.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the start_date
|
||||
pub fn start_date(mut self, start_date: DateTime<Utc>) -> Self {
|
||||
self.start_date = Some(start_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the end_date
|
||||
pub fn end_date(mut self, end_date: DateTime<Utc>) -> Self {
|
||||
self.end_date = Some(end_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set auto_renewal
|
||||
pub fn auto_renewal(mut self, auto_renewal: bool) -> Self {
|
||||
self.auto_renewal = Some(auto_renewal);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the renewal_terms
|
||||
pub fn renewal_terms<S: Into<String>>(mut self, renewal_terms: S) -> Self {
|
||||
self.renewal_terms = Some(renewal_terms.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ContractStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Contract object
|
||||
pub fn build(self) -> Result<Contract, &'static str> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Validate that start_date is before end_date
|
||||
let start_date = self.start_date.ok_or("start_date is required")?;
|
||||
let end_date = self.end_date.ok_or("end_date is required")?;
|
||||
|
||||
if start_date >= end_date {
|
||||
return Err("start_date must be before end_date");
|
||||
}
|
||||
|
||||
Ok(Contract {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_id: self.sale_id,
|
||||
terms: self.terms.ok_or("terms is required")?,
|
||||
start_date,
|
||||
end_date,
|
||||
auto_renewal: self.auto_renewal.unwrap_or(false),
|
||||
renewal_terms: self.renewal_terms.ok_or("renewal_terms is required")?,
|
||||
status: self.status.unwrap_or(ContractStatus::Active),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Storable for Contract {}
|
||||
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Contract {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"contract"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for service_id if present
|
||||
if let Some(service_id) = self.service_id {
|
||||
keys.push(IndexKey {
|
||||
name: "service_id",
|
||||
value: service_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add an index for sale_id if present
|
||||
if let Some(sale_id) = self.sale_id {
|
||||
keys.push(IndexKey {
|
||||
name: "sale_id",
|
||||
value: sale_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for active contracts
|
||||
if self.is_active() {
|
||||
keys.push(IndexKey {
|
||||
name: "active",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
use crate::db::model::{Model, IndexKey};
|
||||
use crate::db::{Storable, DbError, DbResult};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Currency represents a monetary value with amount and currency code
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Currency {
|
||||
pub id: u32,
|
||||
pub amount: f64,
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
impl Currency {
|
||||
/// Create a new currency with amount and code
|
||||
pub fn new(id: u32, amount: f64, currency_code: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
amount,
|
||||
currency_code,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn amount(&mut self) -> f64 {
|
||||
self.amount
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Currency
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct CurrencyBuilder {
|
||||
id: Option<u32>,
|
||||
amount: Option<f64>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl CurrencyBuilder {
|
||||
/// Create a new CurrencyBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
amount: None,
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = Some(amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Currency object
|
||||
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
|
||||
Ok(Currency {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Currency {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Currency {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"currency"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for currency_code
|
||||
keys.push(IndexKey {
|
||||
name: "currency_code",
|
||||
value: self.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add an index for amount range
|
||||
// This allows finding currencies within specific ranges
|
||||
let amount_range = match self.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "amount_range",
|
||||
value: amount_range.to_string(),
|
||||
});
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Customer represents a customer who can purchase products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Customer {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub pubkey: String,
|
||||
pub contact_ids: Vec<u32>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Customer {
|
||||
/// Create a new customer with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
pubkey: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
pubkey,
|
||||
contact_ids: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a contact ID to the customer
|
||||
pub fn add_contact(&mut self, contact_id: u32) {
|
||||
if !self.contact_ids.contains(&contact_id) {
|
||||
self.contact_ids.push(contact_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a contact ID from the customer
|
||||
pub fn remove_contact(&mut self, contact_id: u32) -> bool {
|
||||
let len = self.contact_ids.len();
|
||||
self.contact_ids.retain(|&id| id != contact_id);
|
||||
|
||||
if self.contact_ids.len() < len {
|
||||
self.updated_at = Utc::now();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Customer
|
||||
pub struct CustomerBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
pubkey: Option<String>,
|
||||
contact_ids: Vec<u32>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl CustomerBuilder {
|
||||
/// Create a new CustomerBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
pubkey: None,
|
||||
contact_ids: Vec::new(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the pubkey
|
||||
pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
|
||||
self.pubkey = Some(pubkey.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a contact ID
|
||||
pub fn add_contact(mut self, contact_id: u32) -> Self {
|
||||
self.contact_ids.push(contact_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple contact IDs
|
||||
pub fn contact_ids(mut self, contact_ids: Vec<u32>) -> Self {
|
||||
self.contact_ids = contact_ids;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Customer object
|
||||
pub fn build(self) -> Result<Customer, &'static str> {
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Customer {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
pubkey: self.pubkey.ok_or("pubkey is required")?,
|
||||
contact_ids: self.contact_ids,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Customer {}
|
||||
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Customer {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"customer"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for the name
|
||||
keys.push(IndexKey {
|
||||
name: "name",
|
||||
value: self.name.clone(),
|
||||
});
|
||||
|
||||
// Add an index for the pubkey
|
||||
keys.push(IndexKey {
|
||||
name: "pubkey",
|
||||
value: self.pubkey.clone(),
|
||||
});
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::model::{Model, Storable};
|
||||
|
||||
/// ExchangeRate represents an exchange rate between two currencies
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExchangeRate {
|
||||
pub id: u32,
|
||||
pub base_currency: String,
|
||||
pub target_currency: String,
|
||||
pub rate: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ExchangeRate {
|
||||
/// Create a new exchange rate
|
||||
pub fn new(id: u32, base_currency: String, target_currency: String, rate: f64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
base_currency,
|
||||
target_currency,
|
||||
rate,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for ExchangeRate
|
||||
pub struct ExchangeRateBuilder {
|
||||
id: Option<u32>,
|
||||
base_currency: Option<String>,
|
||||
target_currency: Option<String>,
|
||||
rate: Option<f64>,
|
||||
timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ExchangeRateBuilder {
|
||||
/// Create a new ExchangeRateBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
base_currency: None,
|
||||
target_currency: None,
|
||||
rate: None,
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the base currency
|
||||
pub fn base_currency<S: Into<String>>(mut self, base_currency: S) -> Self {
|
||||
self.base_currency = Some(base_currency.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the target currency
|
||||
pub fn target_currency<S: Into<String>>(mut self, target_currency: S) -> Self {
|
||||
self.target_currency = Some(target_currency.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rate
|
||||
pub fn rate(mut self, rate: f64) -> Self {
|
||||
self.rate = Some(rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp
|
||||
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
|
||||
self.timestamp = Some(timestamp);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ExchangeRate object
|
||||
pub fn build(self) -> Result<ExchangeRate, &'static str> {
|
||||
let now = Utc::now();
|
||||
Ok(ExchangeRate {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
base_currency: self.base_currency.ok_or("base_currency is required")?,
|
||||
target_currency: self.target_currency.ok_or("target_currency is required")?,
|
||||
rate: self.rate.ok_or("rate is required")?,
|
||||
timestamp: self.timestamp.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Storable for ExchangeRate {}
|
||||
// Implement Model trait
|
||||
impl Model for ExchangeRate {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"exchange_rate"
|
||||
}
|
||||
}
|
||||
|
||||
/// ExchangeRateService provides methods to get and set exchange rates
|
||||
#[derive(Clone)]
|
||||
pub struct ExchangeRateService {
|
||||
rates: Arc<Mutex<HashMap<String, ExchangeRate>>>,
|
||||
}
|
||||
|
||||
impl ExchangeRateService {
|
||||
/// Create a new exchange rate service
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rates: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an exchange rate
|
||||
pub fn set_rate(&self, exchange_rate: ExchangeRate) {
|
||||
let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency);
|
||||
let mut rates = self.rates.lock().unwrap();
|
||||
rates.insert(key, exchange_rate);
|
||||
}
|
||||
|
||||
/// Get an exchange rate
|
||||
pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option<ExchangeRate> {
|
||||
let key = format!("{}_{}", base_currency, target_currency);
|
||||
let rates = self.rates.lock().unwrap();
|
||||
rates.get(&key).cloned()
|
||||
}
|
||||
|
||||
/// Convert an amount from one currency to another
|
||||
pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option<f64> {
|
||||
// If the currencies are the same, return the amount
|
||||
if from_currency == to_currency {
|
||||
return Some(amount);
|
||||
}
|
||||
|
||||
// Try to get the direct exchange rate
|
||||
if let Some(rate) = self.get_rate(from_currency, to_currency) {
|
||||
return Some(amount * rate.rate);
|
||||
}
|
||||
|
||||
// Try to get the inverse exchange rate
|
||||
if let Some(rate) = self.get_rate(to_currency, from_currency) {
|
||||
return Some(amount / rate.rate);
|
||||
}
|
||||
|
||||
// Try to convert via USD
|
||||
if from_currency != "USD" && to_currency != "USD" {
|
||||
if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") {
|
||||
return self.convert(from_to_usd, "USD", to_currency);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Create a global instance of the exchange rate service
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = {
|
||||
let service = ExchangeRateService::new();
|
||||
|
||||
// Set some default exchange rates
|
||||
service.set_rate(ExchangeRate::new(1, "USD".to_string(), "EUR".to_string(), 0.85));
|
||||
service.set_rate(ExchangeRate::new(2, "USD".to_string(), "GBP".to_string(), 0.75));
|
||||
service.set_rate(ExchangeRate::new(3, "USD".to_string(), "JPY".to_string(), 110.0));
|
||||
service.set_rate(ExchangeRate::new(4, "USD".to_string(), "CAD".to_string(), 1.25));
|
||||
service.set_rate(ExchangeRate::new(5, "USD".to_string(), "AUD".to_string(), 1.35));
|
||||
|
||||
service
|
||||
};
|
||||
}
|
||||
@@ -1,577 +0,0 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc, Datelike};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// InvoiceStatus represents the status of an invoice
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InvoiceStatus {
|
||||
Draft,
|
||||
Sent,
|
||||
Paid,
|
||||
Overdue,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// PaymentStatus represents the payment status of an invoice
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PaymentStatus {
|
||||
Unpaid,
|
||||
PartiallyPaid,
|
||||
Paid,
|
||||
}
|
||||
|
||||
/// Payment represents a payment made against an invoice
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Payment {
|
||||
pub amount: Currency,
|
||||
pub date: DateTime<Utc>,
|
||||
pub method: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl Payment {
|
||||
/// Create a new payment
|
||||
pub fn new(amount: Currency, method: String, comment: String) -> Self {
|
||||
Self {
|
||||
amount,
|
||||
date: Utc::now(),
|
||||
method,
|
||||
comment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// InvoiceItem represents an item in an invoice
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InvoiceItem {
|
||||
pub id: u32,
|
||||
pub invoice_id: u32,
|
||||
pub description: String,
|
||||
pub amount: Currency,
|
||||
pub service_id: Option<u32>,
|
||||
pub sale_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl InvoiceItem {
|
||||
/// Create a new invoice item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
invoice_id: u32,
|
||||
description: String,
|
||||
amount: Currency,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
invoice_id,
|
||||
description,
|
||||
amount,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Link the invoice item to a service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
}
|
||||
|
||||
/// Link the invoice item to a sale
|
||||
pub fn link_to_sale(&mut self, sale_id: u32) {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for InvoiceItem
|
||||
pub struct InvoiceItemBuilder {
|
||||
id: Option<u32>,
|
||||
invoice_id: Option<u32>,
|
||||
description: Option<String>,
|
||||
amount: Option<Currency>,
|
||||
service_id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl InvoiceItemBuilder {
|
||||
/// Create a new InvoiceItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
invoice_id: None,
|
||||
description: None,
|
||||
amount: None,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the invoice_id
|
||||
pub fn invoice_id(mut self, invoice_id: u32) -> Self {
|
||||
self.invoice_id = Some(invoice_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount
|
||||
pub fn amount(mut self, amount: Currency) -> Self {
|
||||
self.amount = Some(amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the InvoiceItem object
|
||||
pub fn build(self) -> Result<InvoiceItem, &'static str> {
|
||||
Ok(InvoiceItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
invoice_id: self.invoice_id.ok_or("invoice_id is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_id: self.sale_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoice represents an invoice sent to a customer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Invoice {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub total_amount: Currency,
|
||||
pub balance_due: Currency,
|
||||
pub status: InvoiceStatus,
|
||||
pub payment_status: PaymentStatus,
|
||||
pub issue_date: DateTime<Utc>,
|
||||
pub due_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<InvoiceItem>,
|
||||
pub payments: Vec<Payment>,
|
||||
}
|
||||
|
||||
impl Invoice {
|
||||
/// Create a new invoice with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
issue_date: DateTime<Utc>,
|
||||
due_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID for zero amounts
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
total_amount: zero_amount.clone(),
|
||||
balance_due: zero_amount,
|
||||
status: InvoiceStatus::Draft,
|
||||
payment_status: PaymentStatus::Unpaid,
|
||||
issue_date,
|
||||
due_date,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
payments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the invoice and update the total amount
|
||||
pub fn add_item(&mut self, item: InvoiceItem) {
|
||||
// Make sure the item's invoice_id matches this invoice
|
||||
assert_eq!(self.id, item.invoice_id, "Item invoice_id must match invoice id");
|
||||
|
||||
// Update the total amount
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.amount.amount,
|
||||
item.amount.currency_code.clone()
|
||||
);
|
||||
self.balance_due = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.amount.amount,
|
||||
item.amount.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.amount.amount;
|
||||
self.balance_due.amount += item.amount.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the total amount based on all items
|
||||
pub fn calculate_total(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].amount.currency_code.clone();
|
||||
|
||||
// Calculate the total amount
|
||||
let mut total = 0.0;
|
||||
for item in &self.items {
|
||||
total += item.amount.amount;
|
||||
}
|
||||
|
||||
// Update the total amount
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
total,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Recalculate the balance due
|
||||
self.calculate_balance();
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Add a payment to the invoice and update the balance due and payment status
|
||||
pub fn add_payment(&mut self, payment: Payment) {
|
||||
// Update the balance due
|
||||
self.balance_due.amount -= payment.amount.amount;
|
||||
|
||||
// Add the payment to the list
|
||||
self.payments.push(payment);
|
||||
|
||||
// Update the payment status
|
||||
self.update_payment_status();
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the balance due based on total amount and payments
|
||||
pub fn calculate_balance(&mut self) {
|
||||
// Start with the total amount
|
||||
let mut balance = self.total_amount.amount;
|
||||
|
||||
// Subtract all payments
|
||||
for payment in &self.payments {
|
||||
balance -= payment.amount.amount;
|
||||
}
|
||||
|
||||
// Update the balance due
|
||||
self.balance_due = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
balance,
|
||||
self.total_amount.currency_code.clone()
|
||||
);
|
||||
|
||||
// Update the payment status
|
||||
self.update_payment_status();
|
||||
}
|
||||
|
||||
/// Update the payment status based on the balance due
|
||||
fn update_payment_status(&mut self) {
|
||||
if self.balance_due.amount <= 0.0 {
|
||||
self.payment_status = PaymentStatus::Paid;
|
||||
// If fully paid, also update the invoice status
|
||||
if self.status != InvoiceStatus::Cancelled {
|
||||
self.status = InvoiceStatus::Paid;
|
||||
}
|
||||
} else if self.payments.is_empty() {
|
||||
self.payment_status = PaymentStatus::Unpaid;
|
||||
} else {
|
||||
self.payment_status = PaymentStatus::PartiallyPaid;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the invoice
|
||||
pub fn update_status(&mut self, status: InvoiceStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
// If the invoice is cancelled, don't change the payment status
|
||||
if status != InvoiceStatus::Cancelled {
|
||||
// Re-evaluate the payment status
|
||||
self.update_payment_status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the invoice is overdue
|
||||
pub fn is_overdue(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
self.payment_status != PaymentStatus::Paid &&
|
||||
now > self.due_date &&
|
||||
self.status != InvoiceStatus::Cancelled
|
||||
}
|
||||
|
||||
/// Mark the invoice as overdue if it's past the due date
|
||||
pub fn check_if_overdue(&mut self) -> bool {
|
||||
if self.is_overdue() && self.status != InvoiceStatus::Overdue {
|
||||
self.status = InvoiceStatus::Overdue;
|
||||
self.updated_at = Utc::now();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Invoice
|
||||
pub struct InvoiceBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
total_amount: Option<Currency>,
|
||||
balance_due: Option<Currency>,
|
||||
status: Option<InvoiceStatus>,
|
||||
payment_status: Option<PaymentStatus>,
|
||||
issue_date: Option<DateTime<Utc>>,
|
||||
due_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<InvoiceItem>,
|
||||
payments: Vec<Payment>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl InvoiceBuilder {
|
||||
/// Create a new InvoiceBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
total_amount: None,
|
||||
balance_due: None,
|
||||
status: None,
|
||||
payment_status: None,
|
||||
issue_date: None,
|
||||
due_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
payments: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: InvoiceStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the issue_date
|
||||
pub fn issue_date(mut self, issue_date: DateTime<Utc>) -> Self {
|
||||
self.issue_date = Some(issue_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the due_date
|
||||
pub fn due_date(mut self, due_date: DateTime<Utc>) -> Self {
|
||||
self.due_date = Some(due_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the invoice
|
||||
pub fn add_item(mut self, item: InvoiceItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a payment to the invoice
|
||||
pub fn add_payment(mut self, payment: Payment) -> Self {
|
||||
self.payments.push(payment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Invoice object
|
||||
pub fn build(self) -> Result<Invoice, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount and balance due
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate total amount from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's invoice_id matches this invoice
|
||||
if item.invoice_id != id {
|
||||
return Err("Item invoice_id must match invoice id");
|
||||
}
|
||||
|
||||
total_amount.amount += item.amount.amount;
|
||||
}
|
||||
|
||||
// Calculate balance due (total minus payments)
|
||||
let mut balance_due = total_amount.clone();
|
||||
for payment in &self.payments {
|
||||
balance_due.amount -= payment.amount.amount;
|
||||
}
|
||||
|
||||
// Determine payment status
|
||||
let payment_status = if balance_due.amount <= 0.0 {
|
||||
PaymentStatus::Paid
|
||||
} else if self.payments.is_empty() {
|
||||
PaymentStatus::Unpaid
|
||||
} else {
|
||||
PaymentStatus::PartiallyPaid
|
||||
};
|
||||
|
||||
// Determine invoice status if not provided
|
||||
let status = if let Some(status) = self.status {
|
||||
status
|
||||
} else if payment_status == PaymentStatus::Paid {
|
||||
InvoiceStatus::Paid
|
||||
} else {
|
||||
InvoiceStatus::Draft
|
||||
};
|
||||
|
||||
Ok(Invoice {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
balance_due: self.balance_due.unwrap_or(balance_due),
|
||||
status,
|
||||
payment_status,
|
||||
issue_date: self.issue_date.ok_or("issue_date is required")?,
|
||||
due_date: self.due_date.ok_or("due_date is required")?,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
payments: self.payments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Invoice {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Invoice {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"invoice"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for payment_status
|
||||
keys.push(IndexKey {
|
||||
name: "payment_status",
|
||||
value: format!("{:?}", self.payment_status),
|
||||
});
|
||||
|
||||
// Add an index for currency code
|
||||
keys.push(IndexKey {
|
||||
name: "currency",
|
||||
value: self.total_amount.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add an index for amount range
|
||||
let amount_range = match self.total_amount.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "amount_range",
|
||||
value: amount_range.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for issue date (year-month)
|
||||
keys.push(IndexKey {
|
||||
name: "issue_date",
|
||||
value: format!("{}", self.issue_date.format("%Y-%m")),
|
||||
});
|
||||
|
||||
// Add an index for due date (year-month)
|
||||
keys.push(IndexKey {
|
||||
name: "due_date",
|
||||
value: format!("{}", self.due_date.format("%Y-%m")),
|
||||
});
|
||||
|
||||
// Add an index for overdue invoices
|
||||
if self.is_overdue() {
|
||||
keys.push(IndexKey {
|
||||
name: "overdue",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
pub mod user;
|
||||
pub mod vote;
|
||||
pub mod company;
|
||||
pub mod meeting;
|
||||
pub mod product;
|
||||
pub mod sale;
|
||||
pub mod shareholder;
|
||||
// pub mod db; // Moved to src/zaz/db
|
||||
// pub mod migration; // Removed
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use user::User;
|
||||
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub use meeting::Meeting;
|
||||
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
||||
pub use sale::Sale;
|
||||
pub use shareholder::Shareholder;
|
||||
|
||||
// Re-export builder types
|
||||
pub use product::{ProductBuilder, ProductComponentBuilder};
|
||||
pub use sale::{SaleBuilder, SaleItemBuilder};
|
||||
|
||||
// Re-export Currency and its builder
|
||||
pub use product::Currency;
|
||||
pub use currency::CurrencyBuilder;
|
||||
|
||||
// Re-export database components
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};
|
||||
@@ -1,28 +0,0 @@
|
||||
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};
|
||||
@@ -1,425 +0,0 @@
|
||||
use crate::db::model::{Model, IndexKey};
|
||||
use crate::db::Storable;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ProductType represents the type of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProductType {
|
||||
Product,
|
||||
Service,
|
||||
}
|
||||
|
||||
/// ProductStatus represents the status of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProductStatus {
|
||||
Available,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// ProductComponent represents a component of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProductComponent {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub quantity: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ProductComponent {
|
||||
/// Create a new product component with default timestamps
|
||||
pub fn new(id: u32, name: String, description: String, quantity: i64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
quantity,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for ProductComponent
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductComponentBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
quantity: Option<i64>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ProductComponentBuilder {
|
||||
/// Create a new ProductComponentBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
quantity: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i64) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the created_at timestamp
|
||||
pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
|
||||
self.created_at = Some(created_at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the updated_at timestamp
|
||||
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
|
||||
self.updated_at = Some(updated_at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ProductComponent object
|
||||
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
let now = Utc::now();
|
||||
Ok(ProductComponent {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
quantity: self.quantity.ok_or("quantity is required")?,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Product represents a product or service offered in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Product {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: Currency,
|
||||
pub type_: ProductType,
|
||||
pub category: String,
|
||||
pub status: ProductStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub max_amount: i64, // means allows us to define how many max of this there are
|
||||
pub purchase_till: DateTime<Utc>,
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
pub components: Vec<ProductComponent>,
|
||||
}
|
||||
|
||||
impl Product {
|
||||
/// Create a new product with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: i64,
|
||||
validity_days: i64, // How many days the product is valid after purchase
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
// Default: purchasable for 1 year, active for specified validity days after purchase
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
type_,
|
||||
category,
|
||||
status,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
max_amount,
|
||||
purchase_till: now + Duration::days(365),
|
||||
active_till: now + Duration::days(validity_days),
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a component to this product
|
||||
pub fn add_component(&mut self, component: ProductComponent) {
|
||||
self.components.push(component);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the purchase availability timeframe
|
||||
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
|
||||
self.purchase_till = purchase_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the active timeframe
|
||||
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
|
||||
self.active_till = active_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Check if the product is available for purchase
|
||||
pub fn is_purchasable(&self) -> bool {
|
||||
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||
}
|
||||
|
||||
/// Check if the product is still active (for services)
|
||||
pub fn is_active(&self) -> bool {
|
||||
Utc::now() <= self.active_till
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Product
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<Currency>,
|
||||
type_: Option<ProductType>,
|
||||
category: Option<String>,
|
||||
status: Option<ProductStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
max_amount: Option<i64>,
|
||||
purchase_till: Option<DateTime<Utc>>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
components: Vec<ProductComponent>,
|
||||
validity_days: Option<i64>,
|
||||
}
|
||||
|
||||
impl ProductBuilder {
|
||||
/// Create a new ProductBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
price: None,
|
||||
type_: None,
|
||||
category: None,
|
||||
status: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
max_amount: None,
|
||||
purchase_till: None,
|
||||
active_till: None,
|
||||
components: Vec::new(),
|
||||
validity_days: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the price
|
||||
pub fn price(mut self, price: Currency) -> Self {
|
||||
self.price = Some(price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product type
|
||||
pub fn type_(mut self, type_: ProductType) -> Self {
|
||||
self.type_ = Some(type_);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the category
|
||||
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
|
||||
self.category = Some(category.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ProductStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max amount
|
||||
pub fn max_amount(mut self, max_amount: i64) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the validity days
|
||||
pub fn validity_days(mut self, validity_days: i64) -> Self {
|
||||
self.validity_days = Some(validity_days);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the purchase_till date directly
|
||||
pub fn purchase_till(mut self, purchase_till: DateTime<Utc>) -> Self {
|
||||
self.purchase_till = Some(purchase_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till date directly
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a component to the product
|
||||
pub fn add_component(mut self, component: ProductComponent) -> Self {
|
||||
self.components.push(component);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Product object
|
||||
pub fn build(self) -> Result<Product, &'static str> {
|
||||
let now = Utc::now();
|
||||
let created_at = self.created_at.unwrap_or(now);
|
||||
let updated_at = self.updated_at.unwrap_or(now);
|
||||
|
||||
// Calculate purchase_till and active_till based on validity_days if not set directly
|
||||
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
|
||||
let active_till = if let Some(validity_days) = self.validity_days {
|
||||
self.active_till
|
||||
.unwrap_or(now + Duration::days(validity_days))
|
||||
} else {
|
||||
self.active_till
|
||||
.ok_or("Either active_till or validity_days must be provided")?
|
||||
};
|
||||
|
||||
Ok(Product {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
price: self.price.ok_or("price is required")?,
|
||||
type_: self.type_.ok_or("type_ is required")?,
|
||||
category: self.category.ok_or("category is required")?,
|
||||
status: self.status.ok_or("status is required")?,
|
||||
created_at,
|
||||
updated_at,
|
||||
max_amount: self.max_amount.ok_or("max_amount is required")?,
|
||||
purchase_till,
|
||||
active_till,
|
||||
components: self.components,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Product {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Product {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"product"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for name
|
||||
keys.push(IndexKey {
|
||||
name: "name",
|
||||
value: self.name.clone(),
|
||||
});
|
||||
|
||||
// Add an index for category
|
||||
keys.push(IndexKey {
|
||||
name: "category",
|
||||
value: self.category.clone(),
|
||||
});
|
||||
|
||||
// Add an index for product type
|
||||
keys.push(IndexKey {
|
||||
name: "type",
|
||||
value: format!("{:?}", self.type_),
|
||||
});
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for price range
|
||||
let price_range = match self.price.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "price_range",
|
||||
value: price_range.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for currency code
|
||||
keys.push(IndexKey {
|
||||
name: "currency",
|
||||
value: self.price.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add indexes for purchasable and active products
|
||||
if self.is_purchasable() {
|
||||
keys.push(IndexKey {
|
||||
name: "purchasable",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if self.is_active() {
|
||||
keys.push(IndexKey {
|
||||
name: "active",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
// Import Currency from the currency module
|
||||
use crate::models::biz::Currency;
|
||||
2164
herodb_old/src/models/biz/rhai/Cargo.lock
generated
2164
herodb_old/src/models/biz/rhai/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "biz_rhai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rhai = "1.21.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4"
|
||||
herodb = { path = "../../../.." }
|
||||
|
||||
[lib]
|
||||
name = "biz_rhai"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "example"
|
||||
path = "examples/example.rs"
|
||||
@@ -1,168 +0,0 @@
|
||||
// Example script demonstrating the use of Business module operations
|
||||
|
||||
// Custom repeat function since Rhai doesn't have a built-in repeat method
|
||||
fn repeat_str(str, count) {
|
||||
let result = "";
|
||||
for i in 0..count {
|
||||
result += str;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Print a section header
|
||||
fn print_section(title) {
|
||||
let line = repeat_str("=", 50);
|
||||
print(line);
|
||||
print(" " + title);
|
||||
print(line);
|
||||
}
|
||||
|
||||
print_section("BUSINESS MODULE OPERATIONS EXAMPLE");
|
||||
|
||||
// Currency Operations
|
||||
print_section("CURRENCY OPERATIONS");
|
||||
|
||||
// Create a currency
|
||||
print("\nCreating currencies...");
|
||||
let usd = create_currency(100.0, "USD");
|
||||
print("USD Currency created: " + usd.amount + " " + usd.currency_code);
|
||||
|
||||
// Convert currencies
|
||||
print("\nConverting currencies...");
|
||||
let eur = create_currency(150.0, "EUR");
|
||||
print("EUR Currency created: " + eur.amount + " " + eur.currency_code);
|
||||
|
||||
let eur_to_usd = convert_currency(eur, "USD");
|
||||
print("EUR to USD: " + eur.amount + " EUR = " + eur_to_usd.amount + " USD");
|
||||
|
||||
// Product Component Operations
|
||||
print_section("PRODUCT COMPONENT OPERATIONS");
|
||||
|
||||
// Create a product component
|
||||
print("\nCreating product components...");
|
||||
let component1 = create_product_component(1, "CPU", "Intel i7 Processor", 1);
|
||||
print("Component created: " + component1.name);
|
||||
print(" ID: " + component1.id);
|
||||
print(" Description: " + component1.description);
|
||||
print(" Quantity: " + component1.quantity);
|
||||
|
||||
// Create another component
|
||||
let component2 = create_product_component(2, "RAM", "32GB DDR4 Memory", 2);
|
||||
print("Component created: " + component2.name);
|
||||
print(" ID: " + component2.id);
|
||||
print(" Description: " + component2.description);
|
||||
print(" Quantity: " + component2.quantity);
|
||||
|
||||
// Product Operations
|
||||
print_section("PRODUCT OPERATIONS");
|
||||
|
||||
// Create a product using builder pattern
|
||||
print("\nCreating a product...");
|
||||
let product = create_product_builder()
|
||||
.product_name("High-End Gaming PC")
|
||||
.product_description("Ultimate gaming experience")
|
||||
.product_price(create_currency(1999.99, "USD"))
|
||||
.product_validity_days(365)
|
||||
.build();
|
||||
|
||||
print("Product created: " + product.name);
|
||||
print(" ID: " + product.id);
|
||||
print(" Description: " + product.description);
|
||||
print(" Price: " + product.price.amount + " " + product.price.currency_code);
|
||||
|
||||
// Add components to the product
|
||||
print("\nAdding components to product...");
|
||||
let product_with_components = product
|
||||
.add_component(component1)
|
||||
.add_component(component2);
|
||||
|
||||
print("Components added to product");
|
||||
|
||||
// Get components from the product
|
||||
let components = get_product_components(product_with_components);
|
||||
print("Number of components: " + components.len());
|
||||
|
||||
// Customer Operations
|
||||
print_section("CUSTOMER OPERATIONS");
|
||||
|
||||
// Create a customer
|
||||
print("\nCreating a customer...");
|
||||
let customer = create_customer("John Doe", "john@example.com", "+1234567890", "123 Main St");
|
||||
print("Customer created: " + customer.name);
|
||||
print(" Email: " + customer.email);
|
||||
print(" Phone: " + customer.phone);
|
||||
print(" Address: " + customer.address);
|
||||
|
||||
// Sale Operations
|
||||
print_section("SALE OPERATIONS");
|
||||
|
||||
// Create a sale
|
||||
print("\nCreating a sale...");
|
||||
let sale = create_sale(customer, "2025-04-19");
|
||||
print("Sale created for customer: " + sale.customer.name);
|
||||
print(" Date: " + sale.date);
|
||||
|
||||
// Add product to sale
|
||||
let sale_with_item = add_sale_item(sale, product_with_components, 1);
|
||||
print("Added product to sale: " + product_with_components.name);
|
||||
|
||||
// Service Operations
|
||||
print_section("SERVICE OPERATIONS");
|
||||
|
||||
// Create a service
|
||||
print("\nCreating a service...");
|
||||
let service = create_service(
|
||||
"Premium Support",
|
||||
"24/7 Technical Support",
|
||||
create_currency(49.99, "USD"),
|
||||
30
|
||||
);
|
||||
print("Service created: " + service.name);
|
||||
print(" Description: " + service.description);
|
||||
print(" Price: " + service.price.amount + " " + service.price.currency_code);
|
||||
print(" Duration: " + service.duration + " days");
|
||||
|
||||
// Contract Operations
|
||||
print_section("CONTRACT OPERATIONS");
|
||||
|
||||
// Create a contract
|
||||
print("\nCreating a contract...");
|
||||
let contract = create_contract(
|
||||
"Support Agreement",
|
||||
"Annual support contract",
|
||||
"2025-04-19",
|
||||
"2026-04-19",
|
||||
create_currency(599.99, "USD"),
|
||||
"Active"
|
||||
);
|
||||
print("Contract created: " + contract.title);
|
||||
print(" Description: " + contract.description);
|
||||
print(" Start Date: " + contract.start_date);
|
||||
print(" End Date: " + contract.end_date);
|
||||
print(" Value: " + contract.value.amount + " " + contract.value.currency_code);
|
||||
print(" Status: " + contract.status);
|
||||
|
||||
// Invoice Operations
|
||||
print_section("INVOICE OPERATIONS");
|
||||
|
||||
// Create an invoice
|
||||
print("\nCreating an invoice...");
|
||||
let invoice = create_invoice(
|
||||
"INV-2025-001",
|
||||
"2025-04-19",
|
||||
"2025-05-19",
|
||||
customer,
|
||||
create_currency(2499.99, "USD"),
|
||||
"Issued",
|
||||
"Pending"
|
||||
);
|
||||
print("Invoice created: " + invoice.number);
|
||||
print(" Date: " + invoice.date);
|
||||
print(" Due Date: " + invoice.due_date);
|
||||
print(" Customer: " + invoice.customer.name);
|
||||
print(" Amount: " + invoice.amount.amount + " " + invoice.amount.currency_code);
|
||||
print(" Status: " + invoice.status);
|
||||
print(" Payment Status: " + invoice.payment_status);
|
||||
|
||||
print_section("EXAMPLE COMPLETED");
|
||||
print("All business module operations completed successfully!");
|
||||
@@ -1,41 +0,0 @@
|
||||
use std::{fs, path::Path};
|
||||
use biz_rhai::create_rhai_engine;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Business Module Rhai Wrapper Example ===");
|
||||
|
||||
// Create a Rhai engine with business module functionality
|
||||
let mut engine = create_rhai_engine();
|
||||
println!("Successfully created Rhai engine");
|
||||
|
||||
// Get the path to the example.rhai script
|
||||
let script_path = get_script_path()?;
|
||||
println!("Loading script from: {}", script_path.display());
|
||||
|
||||
// Load the script content
|
||||
let script = fs::read_to_string(&script_path)
|
||||
.map_err(|e| format!("Failed to read script file: {}", e))?;
|
||||
|
||||
// Run the script
|
||||
println!("\n=== Running Rhai script ===");
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("\nScript executed successfully!"),
|
||||
Err(e) => println!("\nScript execution error: {}", e),
|
||||
}
|
||||
|
||||
println!("\nExample completed!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_script_path() -> Result<std::path::PathBuf, String> {
|
||||
// When running with cargo run --example, the script will be in the examples directory
|
||||
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("examples")
|
||||
.join("example.rhai");
|
||||
|
||||
if script_path.exists() {
|
||||
Ok(script_path)
|
||||
} else {
|
||||
Err(format!("Could not find example.rhai script at {}", script_path.display()))
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use rhai::{Engine, EvalAltResult, Map, Dynamic, Array};
|
||||
use crate::wrapper::*;
|
||||
use crate::generic_wrapper::ToRhai;
|
||||
|
||||
/// Create a new Rhai engine with business module functionality
|
||||
pub fn create_rhai_engine() -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register business module types and functions
|
||||
register_business_types(&mut engine);
|
||||
|
||||
engine
|
||||
}
|
||||
|
||||
/// Register business module types and functions
|
||||
fn register_business_types(engine: &mut Engine) {
|
||||
// Currency functions
|
||||
engine.register_fn("create_currency", create_currency);
|
||||
engine.register_fn("convert_currency", convert_currency);
|
||||
|
||||
// Product functions
|
||||
engine.register_fn("create_product_builder", create_product_builder);
|
||||
engine.register_fn("product_name", product_builder_name);
|
||||
engine.register_fn("product_description", product_builder_description);
|
||||
engine.register_fn("product_price", product_builder_price);
|
||||
engine.register_fn("product_validity_days", product_builder_validity_days);
|
||||
engine.register_fn("add_component", product_builder_add_component);
|
||||
engine.register_fn("build", product_builder_build);
|
||||
|
||||
// Product component functions
|
||||
engine.register_fn("create_product_component", create_product_component);
|
||||
engine.register_fn("component_name", product_component_name);
|
||||
engine.register_fn("component_description", product_component_description);
|
||||
engine.register_fn("component_quantity", product_component_quantity);
|
||||
|
||||
// Sale functions
|
||||
engine.register_fn("create_sale", create_sale);
|
||||
engine.register_fn("add_sale_item", add_sale_item);
|
||||
engine.register_fn("sale_customer", sale_customer);
|
||||
engine.register_fn("sale_date", sale_date);
|
||||
engine.register_fn("sale_status", sale_status);
|
||||
|
||||
// Customer functions
|
||||
engine.register_fn("create_customer", create_customer);
|
||||
engine.register_fn("customer_name", customer_name);
|
||||
engine.register_fn("customer_email", customer_email);
|
||||
engine.register_fn("customer_phone", customer_phone);
|
||||
engine.register_fn("customer_address", customer_address);
|
||||
|
||||
// Service functions
|
||||
engine.register_fn("create_service", create_service);
|
||||
engine.register_fn("service_name", service_name);
|
||||
engine.register_fn("service_description", service_description);
|
||||
engine.register_fn("service_price", service_price);
|
||||
engine.register_fn("service_duration", service_duration);
|
||||
|
||||
// Contract functions
|
||||
engine.register_fn("create_contract", create_contract);
|
||||
engine.register_fn("contract_title", contract_title);
|
||||
engine.register_fn("contract_description", contract_description);
|
||||
engine.register_fn("contract_start_date", contract_start_date);
|
||||
engine.register_fn("contract_end_date", contract_end_date);
|
||||
engine.register_fn("contract_value", contract_value);
|
||||
engine.register_fn("contract_status", contract_status);
|
||||
|
||||
// Invoice functions
|
||||
engine.register_fn("create_invoice", create_invoice);
|
||||
engine.register_fn("invoice_number", invoice_number);
|
||||
engine.register_fn("invoice_date", invoice_date);
|
||||
engine.register_fn("invoice_due_date", invoice_due_date);
|
||||
engine.register_fn("invoice_customer", invoice_customer);
|
||||
engine.register_fn("invoice_amount", invoice_amount);
|
||||
engine.register_fn("invoice_status", invoice_status);
|
||||
engine.register_fn("invoice_payment_status", invoice_payment_status);
|
||||
|
||||
// Helper function to get components from a product
|
||||
engine.register_fn("get_product_components", |product_map: Map| -> Array {
|
||||
let mut array = Array::new();
|
||||
|
||||
if let Some(components) = product_map.get("components") {
|
||||
if let Some(components_array) = components.clone().try_cast::<Array>() {
|
||||
return components_array;
|
||||
}
|
||||
}
|
||||
|
||||
array
|
||||
});
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use rhai::{Dynamic, Map, Array};
|
||||
|
||||
/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations
|
||||
pub trait ToRhai {
|
||||
/// Convert to a Rhai Dynamic value
|
||||
fn to_rhai(&self) -> Dynamic;
|
||||
}
|
||||
|
||||
// Implementation of ToRhai for Dynamic
|
||||
impl ToRhai for Dynamic {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic trait for wrapping Rust functions to be used with Rhai
|
||||
pub trait RhaiWrapper {
|
||||
/// Wrap a function that takes ownership of self
|
||||
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(Self) -> R,
|
||||
R: ToRhai;
|
||||
|
||||
/// Wrap a function that takes a mutable reference to self
|
||||
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
R: ToRhai;
|
||||
|
||||
/// Wrap a function that takes an immutable reference to self
|
||||
fn wrap<F, R>(&self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&Self) -> R,
|
||||
R: ToRhai;
|
||||
}
|
||||
|
||||
/// Implementation of RhaiWrapper for any type
|
||||
impl<T> RhaiWrapper for T {
|
||||
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
|
||||
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
|
||||
fn wrap<F, R>(&self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Rhai Map to a Rust HashMap
|
||||
pub fn map_to_hashmap(map: &Map) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
for (key, value) in map.iter() {
|
||||
let k = key.clone().to_string();
|
||||
let v = value.clone().to_string();
|
||||
if !k.is_empty() && !v.is_empty() {
|
||||
result.insert(k, v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a HashMap<String, String> to a Rhai Map
|
||||
pub fn hashmap_to_map(map: &HashMap<String, String>) -> Map {
|
||||
let mut result = Map::new();
|
||||
for (key, value) in map.iter() {
|
||||
result.insert(key.clone().into(), Dynamic::from(value.clone()));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a Rhai Array to a Vec of strings
|
||||
pub fn array_to_vec_string(array: &Array) -> Vec<String> {
|
||||
array.iter()
|
||||
.filter_map(|item| {
|
||||
let s = item.clone().to_string();
|
||||
if !s.is_empty() { Some(s) } else { None }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<String>
|
||||
pub fn dynamic_to_string_option(value: &Dynamic) -> Option<String> {
|
||||
if value.is_string() {
|
||||
Some(value.clone().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<u32>
|
||||
pub fn dynamic_to_u32_option(value: &Dynamic) -> Option<u32> {
|
||||
if value.is_int() {
|
||||
Some(value.as_int().unwrap() as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<&str> with lifetime management
|
||||
pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> {
|
||||
if value.is_string() {
|
||||
*storage = value.clone().to_string();
|
||||
Some(storage.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Re-export the utility modules
|
||||
pub mod generic_wrapper;
|
||||
pub mod wrapper;
|
||||
pub mod engine;
|
||||
|
||||
// Re-export the utility traits and functions
|
||||
pub use generic_wrapper::{RhaiWrapper, map_to_hashmap, array_to_vec_string,
|
||||
dynamic_to_string_option, hashmap_to_map};
|
||||
pub use engine::create_rhai_engine;
|
||||
|
||||
// The create_rhai_engine function is now in the engine module
|
||||
@@ -1,640 +0,0 @@
|
||||
//! Rhai wrappers for Business module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Business module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map, Position};
|
||||
use std::collections::HashMap;
|
||||
use crate::generic_wrapper::{ToRhai, RhaiWrapper, map_to_hashmap, dynamic_to_string_option, dynamic_to_u32_option};
|
||||
|
||||
// Import business module types
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use herodb::models::biz::{
|
||||
Currency, CurrencyBuilder,
|
||||
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
|
||||
Customer, CustomerBuilder,
|
||||
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
|
||||
Service, ServiceBuilder, ServiceItem, ServiceItemBuilder, ServiceStatus, BillingFrequency,
|
||||
ExchangeRate, ExchangeRateBuilder, ExchangeRateService, EXCHANGE_RATE_SERVICE,
|
||||
Contract, ContractBuilder, ContractStatus,
|
||||
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment
|
||||
};
|
||||
|
||||
// Business module ToRhai implementations
|
||||
|
||||
// Currency ToRhai implementation
|
||||
impl ToRhai for Currency {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("amount".into(), Dynamic::from(self.amount));
|
||||
map.insert("currency_code".into(), Dynamic::from(self.currency_code.clone()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductType ToRhai implementation
|
||||
impl ToRhai for ProductType {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ProductType::Product => "Product",
|
||||
ProductType::Service => "Service",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductStatus ToRhai implementation
|
||||
impl ToRhai for ProductStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ProductStatus::Active => "Active",
|
||||
ProductStatus::Error => "Error",
|
||||
ProductStatus::EndOfLife => "EndOfLife",
|
||||
ProductStatus::Paused => "Paused",
|
||||
ProductStatus::Available => "Available",
|
||||
ProductStatus::Unavailable => "Unavailable",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductComponent ToRhai implementation
|
||||
impl ToRhai for ProductComponent {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
map.insert("energy_usage".into(), Dynamic::from(self.energy_usage));
|
||||
map.insert("cost".into(), self.cost.to_rhai());
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Product ToRhai implementation
|
||||
impl ToRhai for Product {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("type".into(), self.type_.to_rhai());
|
||||
map.insert("category".into(), Dynamic::from(self.category.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
map.insert("max_amount".into(), Dynamic::from(self.max_amount));
|
||||
map.insert("purchase_till".into(), Dynamic::from(self.purchase_till.to_string()));
|
||||
map.insert("active_till".into(), Dynamic::from(self.active_till.to_string()));
|
||||
|
||||
// Convert components to an array
|
||||
let components_array: Array = self.components.iter()
|
||||
.map(|component| component.to_rhai())
|
||||
.collect();
|
||||
map.insert("components".into(), Dynamic::from(components_array));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// SaleStatus ToRhai implementation
|
||||
impl ToRhai for SaleStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
SaleStatus::Pending => "Pending",
|
||||
SaleStatus::Completed => "Completed",
|
||||
SaleStatus::Cancelled => "Cancelled",
|
||||
SaleStatus::Refunded => "Refunded",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// SaleItem ToRhai implementation
|
||||
impl ToRhai for SaleItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("product_id".into(), Dynamic::from(self.product_id));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("discount".into(), Dynamic::from(self.discount));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Sale ToRhai implementation
|
||||
impl ToRhai for Sale {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Customer ToRhai implementation
|
||||
impl ToRhai for Customer {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("email".into(), Dynamic::from(self.email.clone()));
|
||||
map.insert("phone".into(), Dynamic::from(self.phone.clone()));
|
||||
map.insert("address".into(), Dynamic::from(self.address.clone()));
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStatus ToRhai implementation
|
||||
impl ToRhai for ServiceStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ServiceStatus::Active => "Active",
|
||||
ServiceStatus::Inactive => "Inactive",
|
||||
ServiceStatus::Pending => "Pending",
|
||||
ServiceStatus::Cancelled => "Cancelled",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// BillingFrequency ToRhai implementation
|
||||
impl ToRhai for BillingFrequency {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
BillingFrequency::OneTime => "OneTime",
|
||||
BillingFrequency::Daily => "Daily",
|
||||
BillingFrequency::Weekly => "Weekly",
|
||||
BillingFrequency::Monthly => "Monthly",
|
||||
BillingFrequency::Quarterly => "Quarterly",
|
||||
BillingFrequency::Yearly => "Yearly",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceItem ToRhai implementation
|
||||
impl ToRhai for ServiceItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Service ToRhai implementation
|
||||
impl ToRhai for Service {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
|
||||
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
|
||||
map.insert("billing_frequency".into(), self.billing_frequency.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ContractStatus ToRhai implementation
|
||||
impl ToRhai for ContractStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ContractStatus::Draft => "Draft",
|
||||
ContractStatus::Pending => "Pending",
|
||||
ContractStatus::Active => "Active",
|
||||
ContractStatus::Completed => "Completed",
|
||||
ContractStatus::Cancelled => "Cancelled",
|
||||
ContractStatus::Expired => "Expired",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Contract ToRhai implementation
|
||||
impl ToRhai for Contract {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("title".into(), Dynamic::from(self.title.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
|
||||
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
|
||||
map.insert("value".into(), self.value.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// InvoiceStatus ToRhai implementation
|
||||
impl ToRhai for InvoiceStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
InvoiceStatus::Draft => "Draft",
|
||||
InvoiceStatus::Sent => "Sent",
|
||||
InvoiceStatus::Paid => "Paid",
|
||||
InvoiceStatus::Overdue => "Overdue",
|
||||
InvoiceStatus::Cancelled => "Cancelled",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// PaymentStatus ToRhai implementation
|
||||
impl ToRhai for PaymentStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
PaymentStatus::Pending => "Pending",
|
||||
PaymentStatus::Completed => "Completed",
|
||||
PaymentStatus::Failed => "Failed",
|
||||
PaymentStatus::Refunded => "Refunded",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Payment ToRhai implementation
|
||||
impl ToRhai for Payment {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("amount".into(), self.amount.to_rhai());
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("method".into(), Dynamic::from(self.method.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("reference".into(), Dynamic::from(self.reference.clone()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// InvoiceItem ToRhai implementation
|
||||
impl ToRhai for InvoiceItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("unit_price".into(), self.unit_price.to_rhai());
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Invoice ToRhai implementation
|
||||
impl ToRhai for Invoice {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("due_date".into(), Dynamic::from(self.due_date.to_string()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
// Convert payments to an array
|
||||
let payments_array: Array = self.payments.iter()
|
||||
.map(|payment| payment.to_rhai())
|
||||
.collect();
|
||||
map.insert("payments".into(), Dynamic::from(payments_array));
|
||||
|
||||
map.insert("subtotal".into(), self.subtotal.to_rhai());
|
||||
map.insert("tax".into(), self.tax.to_rhai());
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ExchangeRate ToRhai implementation
|
||||
impl ToRhai for ExchangeRate {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("from_currency".into(), Dynamic::from(self.from_currency.clone()));
|
||||
map.insert("to_currency".into(), Dynamic::from(self.to_currency.clone()));
|
||||
map.insert("rate".into(), Dynamic::from(self.rate));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Business Module Function Wrappers
|
||||
//
|
||||
|
||||
// Currency Functions
|
||||
pub fn currency_new(amount: f64, currency_code: &str) -> Currency {
|
||||
Currency::new(amount, currency_code.to_string())
|
||||
}
|
||||
|
||||
pub fn currency_to_usd(currency: &Currency) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match currency.to_usd() {
|
||||
Some(usd) => Ok(usd.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to convert currency to USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn currency_to_currency(currency: &Currency, target_currency: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match currency.to_currency(target_currency) {
|
||||
Some(converted) => Ok(converted.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert currency to {}", target_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// CurrencyBuilder Functions
|
||||
pub fn currency_builder_new() -> CurrencyBuilder {
|
||||
CurrencyBuilder::new()
|
||||
}
|
||||
|
||||
pub fn currency_builder_amount(builder: CurrencyBuilder, amount: f64) -> CurrencyBuilder {
|
||||
builder.amount(amount)
|
||||
}
|
||||
|
||||
pub fn currency_builder_currency_code(builder: CurrencyBuilder, currency_code: &str) -> CurrencyBuilder {
|
||||
builder.currency_code(currency_code)
|
||||
}
|
||||
|
||||
pub fn currency_builder_build(builder: CurrencyBuilder) -> Result<Currency, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// ProductComponent Functions
|
||||
pub fn product_component_new(id: i64, name: &str, description: &str, quantity: i64) -> ProductComponent {
|
||||
ProductComponent::new(id as u32, name.to_string(), description.to_string(), quantity as i32)
|
||||
}
|
||||
|
||||
pub fn product_component_total_energy_usage(component: &ProductComponent) -> f64 {
|
||||
component.total_energy_usage()
|
||||
}
|
||||
|
||||
pub fn product_component_total_cost(component: &ProductComponent) -> Currency {
|
||||
component.total_cost()
|
||||
}
|
||||
|
||||
// ProductComponentBuilder Functions
|
||||
pub fn product_component_builder_new() -> ProductComponentBuilder {
|
||||
ProductComponentBuilder::new()
|
||||
}
|
||||
|
||||
pub fn product_component_builder_id(builder: ProductComponentBuilder, id: i64) -> ProductComponentBuilder {
|
||||
builder.id(id as u32)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_name(builder: ProductComponentBuilder, name: &str) -> ProductComponentBuilder {
|
||||
builder.name(name)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_description(builder: ProductComponentBuilder, description: &str) -> ProductComponentBuilder {
|
||||
builder.description(description)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_quantity(builder: ProductComponentBuilder, quantity: i64) -> ProductComponentBuilder {
|
||||
builder.quantity(quantity as i32)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_energy_usage(builder: ProductComponentBuilder, energy_usage: f64) -> ProductComponentBuilder {
|
||||
builder.energy_usage(energy_usage)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_cost(builder: ProductComponentBuilder, cost: Currency) -> ProductComponentBuilder {
|
||||
builder.cost(cost)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_build(builder: ProductComponentBuilder) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Product Functions
|
||||
pub fn product_type_product() -> ProductType {
|
||||
ProductType::Product
|
||||
}
|
||||
|
||||
pub fn product_type_service() -> ProductType {
|
||||
ProductType::Service
|
||||
}
|
||||
|
||||
pub fn product_status_active() -> ProductStatus {
|
||||
ProductStatus::Active
|
||||
}
|
||||
|
||||
pub fn product_status_error() -> ProductStatus {
|
||||
ProductStatus::Error
|
||||
}
|
||||
|
||||
pub fn product_status_end_of_life() -> ProductStatus {
|
||||
ProductStatus::EndOfLife
|
||||
}
|
||||
|
||||
pub fn product_status_paused() -> ProductStatus {
|
||||
ProductStatus::Paused
|
||||
}
|
||||
|
||||
pub fn product_status_available() -> ProductStatus {
|
||||
ProductStatus::Available
|
||||
}
|
||||
|
||||
pub fn product_status_unavailable() -> ProductStatus {
|
||||
ProductStatus::Unavailable
|
||||
}
|
||||
|
||||
pub fn product_add_component(product: &mut Product, component: ProductComponent) {
|
||||
product.add_component(component);
|
||||
}
|
||||
|
||||
pub fn product_set_purchase_period(product: &mut Product, purchase_till_days: i64) {
|
||||
let purchase_till = Utc::now() + Duration::days(purchase_till_days);
|
||||
product.set_purchase_period(purchase_till);
|
||||
}
|
||||
|
||||
pub fn product_set_active_period(product: &mut Product, active_till_days: i64) {
|
||||
let active_till = Utc::now() + Duration::days(active_till_days);
|
||||
product.set_active_period(active_till);
|
||||
}
|
||||
|
||||
pub fn product_is_purchasable(product: &Product) -> bool {
|
||||
product.is_purchasable()
|
||||
}
|
||||
|
||||
pub fn product_is_active(product: &Product) -> bool {
|
||||
product.is_active()
|
||||
}
|
||||
|
||||
pub fn product_cost_in_currency(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.cost_in_currency(currency_code) {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to calculate cost in {}", currency_code).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.cost_in_usd() {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to calculate cost in USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_total_energy_usage(product: &Product) -> f64 {
|
||||
product.total_energy_usage()
|
||||
}
|
||||
|
||||
pub fn product_components_cost(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.components_cost(currency_code) {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to calculate components cost in {}", currency_code).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_components_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.components_cost_in_usd() {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to calculate components cost in USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ProductBuilder Functions
|
||||
pub fn product_builder_new() -> ProductBuilder {
|
||||
ProductBuilder::new()
|
||||
}
|
||||
|
||||
pub fn product_builder_id(builder: ProductBuilder, id: i64) -> ProductBuilder {
|
||||
builder.id(id as u32)
|
||||
}
|
||||
|
||||
pub fn product_builder_name(builder: ProductBuilder, name: &str) -> ProductBuilder {
|
||||
builder.name(name)
|
||||
}
|
||||
|
||||
pub fn product_builder_description(builder: ProductBuilder, description: &str) -> ProductBuilder {
|
||||
builder.description(description)
|
||||
}
|
||||
|
||||
pub fn product_builder_price(builder: ProductBuilder, price: Currency) -> ProductBuilder {
|
||||
builder.price(price)
|
||||
}
|
||||
|
||||
pub fn product_builder_type(builder: ProductBuilder, type_: ProductType) -> ProductBuilder {
|
||||
builder.type_(type_)
|
||||
}
|
||||
|
||||
pub fn product_builder_category(builder: ProductBuilder, category: &str) -> ProductBuilder {
|
||||
builder.category(category)
|
||||
}
|
||||
|
||||
pub fn product_builder_status(builder: ProductBuilder, status: ProductStatus) -> ProductBuilder {
|
||||
builder.status(status)
|
||||
}
|
||||
|
||||
pub fn product_builder_max_amount(builder: ProductBuilder, max_amount: i64) -> ProductBuilder {
|
||||
builder.max_amount(max_amount as u16)
|
||||
}
|
||||
|
||||
pub fn product_builder_validity_days(builder: ProductBuilder, validity_days: i64) -> ProductBuilder {
|
||||
builder.validity_days(validity_days)
|
||||
}
|
||||
|
||||
pub fn product_builder_add_component(builder: ProductBuilder, component: ProductComponent) -> ProductBuilder {
|
||||
builder.add_component(component)
|
||||
}
|
||||
|
||||
pub fn product_builder_build(builder: ProductBuilder) -> Result<Product, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Exchange Rate Service Functions
|
||||
pub fn exchange_rate_convert(amount: f64, from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
|
||||
match EXCHANGE_RATE_SERVICE.convert(amount, from_currency, to_currency) {
|
||||
Some(converted) => Ok(converted),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert {} {} to {}", amount, from_currency, to_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exchange_rate_get_rate(from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
|
||||
match EXCHANGE_RATE_SERVICE.get_rate(from_currency, to_currency) {
|
||||
Some(rate) => Ok(rate),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to get exchange rate from {} to {}", from_currency, to_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
use crate::db::{Model, Storable, DbError, DbResult, IndexKey};
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
use chrono::{DateTime, Utc};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
/// SaleStatus represents the status of a sale
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SaleStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// SaleItem represents an item in a sale
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SaleItem {
|
||||
pub id: u32,
|
||||
pub sale_id: u32,
|
||||
pub product_id: Option<u32>, // ID of the product sold (if this is a product sale)
|
||||
pub service_id: Option<u32>, // ID of the service sold (if this is a service sale)
|
||||
pub name: String,
|
||||
pub description: String, // Description of the item
|
||||
pub comments: String, // Additional comments about the item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
pub tax_amount: Currency, // Calculated tax amount
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
}
|
||||
|
||||
impl SaleItem {
|
||||
/// Create a new sale item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
sale_id: u32,
|
||||
product_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Validate that either product_id or service_id is provided, but not both
|
||||
assert!(
|
||||
(product_id.is_some() && service_id.is_none()) ||
|
||||
(product_id.is_none() && service_id.is_some()),
|
||||
"Either product_id or service_id must be provided, but not both"
|
||||
);
|
||||
// Calculate subtotal (before tax)
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_amount_value,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
sale_id,
|
||||
product_id,
|
||||
service_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total amount including tax
|
||||
pub fn total_with_tax(&self) -> Currency {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
self.subtotal.amount + self.tax_amount.amount,
|
||||
self.subtotal.currency_code.clone()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SaleItem
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleItemBuilder {
|
||||
id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl SaleItemBuilder {
|
||||
/// Create a new SaleItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
sale_id: None,
|
||||
product_id: None,
|
||||
service_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product_id
|
||||
pub fn product_id(mut self, product_id: Option<u32>) -> Self {
|
||||
// If setting product_id, ensure service_id is None
|
||||
if product_id.is_some() {
|
||||
self.service_id = None;
|
||||
}
|
||||
self.product_id = product_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: Option<u32>) -> Self {
|
||||
// If setting service_id, ensure product_id is None
|
||||
if service_id.is_some() {
|
||||
self.product_id = None;
|
||||
}
|
||||
self.service_id = service_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the unit_price
|
||||
pub fn unit_price(mut self, unit_price: Currency) -> Self {
|
||||
self.unit_price = Some(unit_price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SaleItem object
|
||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
|
||||
|
||||
// Validate that either product_id or service_id is provided, but not both
|
||||
if self.product_id.is_none() && self.service_id.is_none() {
|
||||
return Err("Either product_id or service_id must be provided");
|
||||
}
|
||||
if self.product_id.is_some() && self.service_id.is_some() {
|
||||
return Err("Only one of product_id or service_id can be provided");
|
||||
}
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_amount_value,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Ok(SaleItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
sale_id: self.sale_id.ok_or("sale_id is required")?,
|
||||
product_id: self.product_id,
|
||||
service_id: self.service_id,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sale represents a sale of products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Sale {
|
||||
pub id: u32,
|
||||
pub customer_id: u32, // ID of the customer making the purchase
|
||||
pub subtotal_amount: Currency, // Total before tax
|
||||
pub tax_amount: Currency, // Total tax
|
||||
pub total_amount: Currency, // Total including tax
|
||||
pub status: SaleStatus,
|
||||
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||
pub sale_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<SaleItem>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
|
||||
impl Sale {
|
||||
/// Create a new sale with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
status: SaleStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_currency = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
subtotal_amount: zero_currency.clone(),
|
||||
tax_amount: zero_currency.clone(),
|
||||
total_amount: zero_currency,
|
||||
status,
|
||||
service_id: None,
|
||||
sale_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the sale and update the total amount
|
||||
pub fn add_item(&mut self, item: SaleItem) {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
||||
|
||||
// Update the amounts
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the amounts with the same currency
|
||||
self.subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
self.tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.tax_amount.amount,
|
||||
item.tax_amount.currency_code.clone()
|
||||
);
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount + item.tax_amount.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing totals
|
||||
// (Assumes all items have the same currency)
|
||||
self.subtotal_amount.amount += item.subtotal.amount;
|
||||
self.tax_amount.amount += item.tax_amount.amount;
|
||||
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
// Update the sale timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Recalculate all totals based on items
|
||||
pub fn recalculate_totals(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].subtotal.currency_code.clone();
|
||||
|
||||
// Calculate the totals
|
||||
let mut subtotal = 0.0;
|
||||
let mut tax_total = 0.0;
|
||||
|
||||
for item in &self.items {
|
||||
subtotal += item.subtotal.amount;
|
||||
tax_total += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Update the amounts
|
||||
self.subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal,
|
||||
currency_code.clone()
|
||||
);
|
||||
self.tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_total,
|
||||
currency_code.clone()
|
||||
);
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal + tax_total,
|
||||
currency_code
|
||||
);
|
||||
|
||||
// Update the timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the sale
|
||||
pub fn update_status(&mut self, status: SaleStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link this sale to an existing service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Sale
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleBuilder {
|
||||
id: Option<u32>,
|
||||
company_id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
buyer_name: Option<String>,
|
||||
buyer_email: Option<String>,
|
||||
subtotal_amount: Option<Currency>,
|
||||
tax_amount: Option<Currency>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<SaleStatus>,
|
||||
service_id: Option<u32>,
|
||||
sale_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<SaleItem>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl SaleBuilder {
|
||||
/// Create a new SaleBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
company_id: None,
|
||||
customer_id: None,
|
||||
buyer_name: None,
|
||||
buyer_email: None,
|
||||
subtotal_amount: None,
|
||||
tax_amount: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
service_id: None,
|
||||
sale_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the company_id
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = Some(company_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_name
|
||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||
self.buyer_name = Some(buyer_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_email
|
||||
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
|
||||
self.buyer_email = Some(buyer_email.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: SaleStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_date
|
||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||
self.sale_date = Some(sale_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the sale
|
||||
pub fn add_item(mut self, item: SaleItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Sale object
|
||||
pub fn build(self) -> Result<Sale, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty amounts
|
||||
let mut subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
let mut tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate amounts from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
if item.sale_id != id {
|
||||
return Err("Item sale_id must match sale id");
|
||||
}
|
||||
|
||||
subtotal_amount.amount += item.subtotal.amount;
|
||||
tax_amount.amount += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||
|
||||
Ok(Sale {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
|
||||
tax_amount: self.tax_amount.unwrap_or(tax_amount),
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_date: self.sale_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Sale {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Sale {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"sale"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::{Model, Storable, DbError, DbResult, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// BillingFrequency represents the frequency of billing for a service
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BillingFrequency {
|
||||
Hourly,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Yearly,
|
||||
}
|
||||
|
||||
/// ServiceStatus represents the status of a service
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServiceStatus {
|
||||
Active,
|
||||
Paused,
|
||||
Cancelled,
|
||||
Completed,
|
||||
}
|
||||
|
||||
/// ServiceItem represents an item in a service
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceItem {
|
||||
pub id: u32,
|
||||
pub service_id: u32,
|
||||
pub product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the service item
|
||||
pub comments: String, // Additional comments about the service item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub active_till: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ServiceItem {
|
||||
/// Create a new service item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
service_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
service_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the subtotal based on quantity and unit price
|
||||
pub fn calculate_subtotal(&mut self) {
|
||||
let amount = self.unit_price.amount * self.quantity as f64;
|
||||
self.subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
self.unit_price.currency_code.clone()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Builder for ServiceItem
|
||||
pub struct ServiceItemBuilder {
|
||||
id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
is_taxable: Option<bool>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ServiceItemBuilder {
|
||||
/// Create a new ServiceItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
service_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
is_taxable: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product_id
|
||||
pub fn product_id(mut self, product_id: u32) -> Self {
|
||||
self.product_id = Some(product_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the unit_price
|
||||
pub fn unit_price(mut self, unit_price: Currency) -> Self {
|
||||
self.unit_price = Some(unit_price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set is_taxable
|
||||
pub fn is_taxable(mut self, is_taxable: bool) -> Self {
|
||||
self.is_taxable = Some(is_taxable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ServiceItem object
|
||||
pub fn build(self) -> Result<ServiceItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0);
|
||||
let is_taxable = self.is_taxable.unwrap_or(false);
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount if taxable
|
||||
let tax_amount = if is_taxable {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal.amount * tax_rate,
|
||||
unit_price.currency_code.clone()
|
||||
)
|
||||
} else {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
unit_price.currency_code.clone()
|
||||
)
|
||||
};
|
||||
|
||||
Ok(ServiceItem {
|
||||
id: self.id.ok_or("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")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Service represents a recurring service with billing frequency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub total_amount: Currency,
|
||||
pub status: ServiceStatus,
|
||||
pub billing_frequency: BillingFrequency,
|
||||
pub service_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<ServiceItem>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Create a new service with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
status: ServiceStatus,
|
||||
billing_frequency: BillingFrequency,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
total_amount: Currency::new(0, 0.0, currency_code),
|
||||
status,
|
||||
billing_frequency,
|
||||
service_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the service and update the total amount
|
||||
pub fn add_item(&mut self, item: ServiceItem) {
|
||||
// Make sure the item's service_id matches this service
|
||||
assert_eq!(self.id, item.service_id, "Item service_id must match service id");
|
||||
|
||||
// Update the total amount
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount ,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} 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.push(item);
|
||||
|
||||
// Update the service timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the total amount based on all items
|
||||
pub fn calculate_total(&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 total amount
|
||||
let mut total = 0.0;
|
||||
for item in &self.items {
|
||||
total += item.subtotal.amount;
|
||||
}
|
||||
|
||||
// Update the total amount
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
total,
|
||||
currency_code
|
||||
);
|
||||
|
||||
// Update the service timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the service
|
||||
pub fn update_status(&mut self, status: ServiceStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Service
|
||||
pub struct ServiceBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<ServiceStatus>,
|
||||
billing_frequency: Option<BillingFrequency>,
|
||||
service_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<ServiceItem>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl ServiceBuilder {
|
||||
/// Create a new ServiceBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
billing_frequency: None,
|
||||
service_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ServiceStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the billing_frequency
|
||||
pub fn billing_frequency(mut self, billing_frequency: BillingFrequency) -> Self {
|
||||
self.billing_frequency = Some(billing_frequency);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_date
|
||||
pub fn service_date(mut self, service_date: DateTime<Utc>) -> Self {
|
||||
self.service_date = Some(service_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the service
|
||||
pub fn add_item(mut self, item: ServiceItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Service object
|
||||
pub fn build(self) -> Result<Service, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate total amount from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's service_id matches this service
|
||||
if item.service_id != id {
|
||||
return Err("Item service_id must match service id");
|
||||
}
|
||||
|
||||
if total_amount.amount == 0.0 {
|
||||
// First item, initialize the total amount with the same currency
|
||||
total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
total_amount.amount += item.subtotal.amount ;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Service {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
billing_frequency: self.billing_frequency.ok_or("billing_frequency is required")?,
|
||||
service_date: self.service_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Service {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Service {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"service"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# Circles Core Models
|
||||
|
||||
This directory contains the core data structures used in the herolib circles module. These models serve as the foundation for the circles functionality, providing essential data structures for circles and name management.
|
||||
|
||||
## Overview
|
||||
|
||||
The core models implement the Serde traits (Serialize/Deserialize) and crate database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
|
||||
|
||||
- A struct definition with appropriate fields
|
||||
- Serde serialization through derive macros
|
||||
- Methods for database integration through the SledModel trait
|
||||
- Utility methods for common operations
|
||||
|
||||
## Core Models
|
||||
|
||||
### Circle (`circle.rs`)
|
||||
|
||||
The Circle model represents a collection of members (users or other circles):
|
||||
|
||||
- **Circle**: Main struct with fields for identification and member management
|
||||
- **Member**: Represents a member of a circle with personal information and role
|
||||
- **Role**: Enum for possible member roles (Admin, Stakeholder, Member, Contributor, Guest)
|
||||
|
||||
### Name (`name.rs`)
|
||||
|
||||
The Name model provides DNS record management:
|
||||
|
||||
- **Name**: Main struct for domain management with records and administrators
|
||||
- **Record**: Represents a DNS record with name, text, category, and addresses
|
||||
- **RecordType**: Enum for DNS record types (A, AAAA, CNAME, MX, etc.)
|
||||
|
||||
## Usage
|
||||
|
||||
These models are used by the circles module to manage circles and DNS records. They are typically accessed through the database handlers that implement the generic SledDB interface.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Circle represents a collection of members (users or other circles)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Circle {
|
||||
pub id: u32, // unique id
|
||||
pub name: String, // name of the circle
|
||||
pub description: String, // optional description
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
/// Create a new circle
|
||||
pub fn new(id: u32, name: String, description: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a map of index keys for this circle
|
||||
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
|
||||
impl Storable for Circle {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Circle {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"circle"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
pub mod circle;
|
||||
pub mod name;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use circle::{Circle, Member, Role};
|
||||
pub use name::{Name, Record, RecordType};
|
||||
|
||||
// Re-export database components
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};
|
||||
@@ -1,83 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
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
|
||||
impl Storable for Member {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Member {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"member"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
pub mod circle;
|
||||
pub mod member;
|
||||
pub mod name;
|
||||
pub mod wallet;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use circle::Circle;
|
||||
pub use member::{Member, Role};
|
||||
pub use name::{Name, Record, RecordType};
|
||||
pub use wallet::{Wallet, Asset};
|
||||
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};
|
||||
@@ -1,73 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
|
||||
/// Record types for a DNS record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RecordType {
|
||||
A,
|
||||
AAAA,
|
||||
CNAME,
|
||||
MX,
|
||||
NS,
|
||||
PTR,
|
||||
SOA,
|
||||
SRV,
|
||||
TXT,
|
||||
}
|
||||
|
||||
/// Represents a DNS record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Record {
|
||||
pub name: String, // name of the record
|
||||
pub text: String,
|
||||
pub category: RecordType, // role of the member in the circle
|
||||
pub addr: Vec<String>, // the multiple ipaddresses for this record
|
||||
}
|
||||
|
||||
/// Name represents a DNS domain and its records
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Name {
|
||||
pub id: u32, // unique id
|
||||
pub domain: String,
|
||||
pub description: String, // optional description
|
||||
pub records: Vec<Record>, // DNS records
|
||||
pub admins: Vec<String>, // pubkeys who can change it
|
||||
}
|
||||
|
||||
impl Name {
|
||||
/// Create a new domain name entry
|
||||
pub fn new(id: u32, domain: String, description: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
domain,
|
||||
description,
|
||||
records: Vec::new(),
|
||||
admins: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a record to this domain name
|
||||
pub fn add_record(&mut self, record: Record) {
|
||||
self.records.push(record);
|
||||
}
|
||||
|
||||
/// Add an admin pubkey
|
||||
pub fn add_admin(&mut self, pubkey: String) {
|
||||
self.admins.push(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Name {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Name {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"name"
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DbError, DbResult};
|
||||
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
|
||||
impl Storable for Wallet {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Wallet {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"wallet"
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
# Corporate Governance Module
|
||||
|
||||
This directory contains the core data structures used for corporate governance functionality. These models serve as the foundation for managing companies, shareholders, meetings, voting, resolutions, committees, and more in any organizational context.
|
||||
|
||||
## Overview
|
||||
|
||||
The governance models implement the Serde traits (Serialize/Deserialize) and database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
|
||||
|
||||
- A struct definition with appropriate fields
|
||||
- Serde serialization through derive macros
|
||||
- Methods for database integration through the SledModel trait
|
||||
- Utility methods for common operations
|
||||
|
||||
## Core Models
|
||||
|
||||
### Company (`company.rs`)
|
||||
|
||||
The Company model represents a company entity with its basic information:
|
||||
|
||||
- **Company**: Main struct with fields for company information
|
||||
- Basic details: name, registration number, incorporation date
|
||||
- Contact information: email, phone, website, address
|
||||
- Business information: business type, industry, description
|
||||
- Status tracking: current status, timestamps
|
||||
- **CompanyStatus**: Enum for possible company statuses (Active, Inactive, Suspended)
|
||||
- **BusinessType**: String-based type with validation for business types (Corporation, Partnership, LLC, etc.)
|
||||
|
||||
Key methods:
|
||||
- `add_shareholder()`: Add a shareholder to the company
|
||||
- `link_to_circle()`: Link the company to a Circle for access control
|
||||
- `link_to_customer()`: Link the company to a Customer in the biz module
|
||||
- `get_resolutions()`: Get all resolutions for this company
|
||||
|
||||
### Shareholder (`shareholder.rs`)
|
||||
|
||||
The Shareholder model represents a shareholder of a company:
|
||||
|
||||
- **Shareholder**: Main struct with fields for shareholder information
|
||||
- Identifiers: id, company_id, user_id
|
||||
- Ownership details: shares, percentage
|
||||
- Type and timestamps: shareholder type, since date, created/updated timestamps
|
||||
- **ShareholderType**: Enum for possible shareholder types (Individual, Corporate)
|
||||
|
||||
Key methods:
|
||||
- `update_shares()`: Update the shares owned by this shareholder
|
||||
|
||||
### Meeting (`meeting.rs`)
|
||||
|
||||
The Meeting model represents a board meeting of a company:
|
||||
|
||||
- **Meeting**: Main struct with fields for meeting information
|
||||
- Basic details: id, company_id, title, date, location, description
|
||||
- Status and content: meeting status, minutes
|
||||
- Timestamps and attendees: created/updated timestamps, list of attendees
|
||||
- **Attendee**: Represents an attendee of a meeting
|
||||
- Details: id, meeting_id, user_id, name, role, status, created timestamp
|
||||
- **MeetingStatus**: Enum for possible meeting statuses (Scheduled, Completed, Cancelled)
|
||||
- **AttendeeRole**: Enum for possible attendee roles (Coordinator, Member, Secretary, etc.)
|
||||
- **AttendeeStatus**: Enum for possible attendee statuses (Confirmed, Pending, Declined)
|
||||
|
||||
Key methods:
|
||||
- `add_attendee()`: Add an attendee to the meeting
|
||||
- `update_status()`: Update the status of the meeting
|
||||
- `update_minutes()`: Update the meeting minutes
|
||||
- `find_attendee_by_user_id()`: Find an attendee by user ID
|
||||
- `confirmed_attendees()`: Get all confirmed attendees
|
||||
- `link_to_event()`: Link the meeting to a Calendar Event
|
||||
- `get_resolutions()`: Get all resolutions discussed in this meeting
|
||||
|
||||
### User (`user.rs`)
|
||||
|
||||
The User model represents a user in the governance system:
|
||||
|
||||
- **User**: Main struct with fields for user information
|
||||
- Basic details: id, name, email, password
|
||||
- Role information: company, role
|
||||
- Timestamps: created/updated timestamps
|
||||
|
||||
### Vote (`vote.rs`)
|
||||
|
||||
The Vote model represents a voting item for corporate decision-making:
|
||||
|
||||
- **Vote**: Main struct with fields for vote information
|
||||
- Basic details: id, company_id, title, description
|
||||
- Timing: start_date, end_date
|
||||
- Status and timestamps: vote status, created/updated timestamps
|
||||
- Options and results: list of vote options, list of ballots, private group
|
||||
- **VoteOption**: Represents an option in a vote
|
||||
- Details: id, vote_id, text, count, min_valid
|
||||
- **Ballot**: Represents a ballot cast by a user
|
||||
- Details: id, vote_id, user_id, vote_option_id, shares_count, created timestamp
|
||||
- **VoteStatus**: Enum for possible vote statuses (Open, Closed, Cancelled)
|
||||
|
||||
Key methods:
|
||||
- `add_option()`: Add a voting option to this vote
|
||||
- `add_ballot()`: Add a ballot to this vote
|
||||
- `get_resolution()`: Get the resolution associated with this vote
|
||||
|
||||
### Resolution (`resolution.rs`)
|
||||
|
||||
The Resolution model represents a board resolution:
|
||||
|
||||
- **Resolution**: Main struct with fields for resolution information
|
||||
- Identifiers: id, company_id, meeting_id, vote_id
|
||||
- Content: title, description, text
|
||||
- Status and tracking: resolution status, proposed_by, proposed_at, approved_at, rejected_at
|
||||
- Timestamps and approvals: created/updated timestamps, list of approvals
|
||||
- **Approval**: Represents an approval of a resolution by a board member
|
||||
- Details: id, resolution_id, user_id, name, approved, comments, created timestamp
|
||||
- **ResolutionStatus**: Enum for possible resolution statuses (Draft, Proposed, Approved, Rejected, Withdrawn)
|
||||
|
||||
Key methods:
|
||||
- `propose()`: Propose the resolution
|
||||
- `approve()`: Approve the resolution
|
||||
- `reject()`: Reject the resolution
|
||||
- `withdraw()`: Withdraw the resolution
|
||||
- `add_approval()`: Add an approval to the resolution
|
||||
- `find_approval_by_user_id()`: Find an approval by user ID
|
||||
- `get_approvals()`: Get all approvals
|
||||
- `approval_count()`: Get approval count
|
||||
- `rejection_count()`: Get rejection count
|
||||
- `link_to_meeting()`: Link this resolution to a meeting
|
||||
- `link_to_vote()`: Link this resolution to a vote
|
||||
- `get_meeting()`: Get the meeting associated with this resolution
|
||||
- `get_vote()`: Get the vote associated with this resolution
|
||||
|
||||
### Committee (`committee.rs`)
|
||||
|
||||
The Committee model represents a board committee:
|
||||
|
||||
- **Committee**: Main struct with fields for committee information
|
||||
- Basic details: id, company_id, name, description, purpose
|
||||
- Integration: circle_id
|
||||
- Timestamps and members: created/updated timestamps, list of members
|
||||
- **CommitteeMember**: Represents a member of a committee
|
||||
- Details: id, committee_id, user_id, name, role, since, created timestamp
|
||||
- **CommitteeRole**: Enum for possible committee roles (Chair, ViceChair, Secretary, Member, Advisor, Observer)
|
||||
|
||||
Key methods:
|
||||
- `add_member()`: Add a member to the committee
|
||||
- `find_member_by_user_id()`: Find a member by user ID
|
||||
- `remove_member()`: Remove a member from the committee
|
||||
- `link_to_circle()`: Link this committee to a Circle for access control
|
||||
- `get_member_users()`: Get all users who are members of this committee
|
||||
|
||||
## Model Relationships
|
||||
|
||||
The following diagram illustrates the relationships between the governance models:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Company --> |has many| Shareholder
|
||||
Company --> |has many| Meeting
|
||||
Company --> |has many| Resolution
|
||||
Company --> |has many| Vote
|
||||
Company --> |has many| Committee
|
||||
|
||||
Meeting --> |has many| Attendee
|
||||
Attendee --> |is a| User
|
||||
|
||||
Resolution --> |can be linked to| Meeting
|
||||
Resolution --> |can be linked to| Vote
|
||||
Resolution --> |has many| Approval
|
||||
|
||||
Vote --> |has many| VoteOption
|
||||
Vote --> |has many| Ballot
|
||||
Ballot --> |cast by| User
|
||||
|
||||
Committee --> |has many| CommitteeMember
|
||||
CommitteeMember --> |is a| User
|
||||
```
|
||||
|
||||
## Key Relationships
|
||||
|
||||
- **Company-Shareholder**: A company has multiple shareholders who own shares in the company
|
||||
- **Company-Meeting**: A company holds multiple meetings for governance purposes
|
||||
- **Company-Resolution**: A company creates resolutions that need to be approved
|
||||
- **Company-Vote**: A company conducts votes on various matters
|
||||
- **Company-Committee**: A company can have multiple committees for specialized governance functions
|
||||
- **Meeting-Resolution**: Resolutions can be discussed and approved in meetings
|
||||
- **Resolution-Vote**: Resolutions can be subject to formal voting
|
||||
- **User-Governance**: Users participate in governance as shareholders, meeting attendees, committee members, and by casting votes
|
||||
|
||||
## Integration with Other Modules
|
||||
|
||||
The governance module integrates with other modules in the system:
|
||||
|
||||
### Integration with Biz Module
|
||||
|
||||
- **Company-Customer**: Companies can be linked to customers in the biz module
|
||||
- **Company-Contract**: Companies can be linked to contracts in the biz module
|
||||
- **Shareholder-Customer**: Shareholders can be linked to customers in the biz module
|
||||
- **Meeting-Invoice**: Meetings can be linked to invoices for expense tracking
|
||||
|
||||
### Integration with MCC Module
|
||||
|
||||
- **Meeting-Calendar/Event**: Meetings can be linked to calendar events in the mcc module
|
||||
- **User-Contact**: Users can be linked to contacts in the mcc module
|
||||
- **Vote-Message**: Votes can be linked to messages for notifications
|
||||
|
||||
### Integration with Circle Module
|
||||
|
||||
- **Company-Circle**: Companies can be linked to circles for group-based access control
|
||||
- **User-Member**: Users can be linked to members for role-based permissions
|
||||
|
||||
## Detailed Data Model
|
||||
|
||||
A more detailed class diagram showing the fields and methods of each model:
|
||||
|
||||
```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()
|
||||
}
|
||||
|
||||
class Committee {
|
||||
+u32 id
|
||||
+u32 company_id
|
||||
+String name
|
||||
+String description
|
||||
+String purpose
|
||||
+Option~u32~ circle_id
|
||||
+DateTime created_at
|
||||
+DateTime updated_at
|
||||
+Vec~CommitteeMember~ members
|
||||
+add_member()
|
||||
+find_member_by_user_id()
|
||||
+remove_member()
|
||||
+link_to_circle()
|
||||
+get_member_users()
|
||||
}
|
||||
|
||||
Company "1" -- "many" Shareholder: has
|
||||
Company "1" -- "many" Meeting: holds
|
||||
Company "1" -- "many" Vote: conducts
|
||||
Company "1" -- "many" Resolution: issues
|
||||
Company "1" -- "many" Committee: establishes
|
||||
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
|
||||
Committee "1" -- "many" CommitteeMember: has
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
These models are used by the governance module to manage corporate governance. They are typically accessed through the database handlers that implement the generic SledDB interface.
|
||||
@@ -1,149 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
||||
use crate::models::gov::User;
|
||||
|
||||
/// CommitteeRole represents the role of a member in a committee
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CommitteeRole {
|
||||
Chair,
|
||||
ViceChair,
|
||||
Secretary,
|
||||
Member,
|
||||
Advisor,
|
||||
Observer,
|
||||
}
|
||||
|
||||
/// CommitteeMember represents a member of a committee
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct CommitteeMember {
|
||||
pub id: u32,
|
||||
pub committee_id: u32,
|
||||
pub user_id: u32,
|
||||
pub name: String,
|
||||
pub role: CommitteeRole,
|
||||
pub since: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Committee represents a board committee
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Committee {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub purpose: String,
|
||||
pub circle_id: Option<u32>, // Link to Circle for access control
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub members: Vec<CommitteeMember>,
|
||||
}
|
||||
|
||||
impl Committee {
|
||||
/// Create a new committee with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
purpose: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
name,
|
||||
description,
|
||||
purpose,
|
||||
circle_id: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
members: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a member to the committee
|
||||
pub fn add_member(&mut self, user_id: u32, name: String, role: CommitteeRole) -> &CommitteeMember {
|
||||
let id = if self.members.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.members.iter().map(|m| m.id).max().unwrap_or(0) + 1
|
||||
};
|
||||
|
||||
let now = Utc::now();
|
||||
let member = CommitteeMember {
|
||||
id,
|
||||
committee_id: self.id,
|
||||
user_id,
|
||||
name,
|
||||
role,
|
||||
since: now,
|
||||
created_at: now,
|
||||
};
|
||||
|
||||
self.members.push(member);
|
||||
self.updated_at = now;
|
||||
self.members.last().unwrap()
|
||||
}
|
||||
|
||||
/// Find a member by user ID
|
||||
pub fn find_member_by_user_id(&self, user_id: u32) -> Option<&CommitteeMember> {
|
||||
self.members.iter().find(|m| m.user_id == user_id)
|
||||
}
|
||||
|
||||
/// Find a member by user ID (mutable version)
|
||||
pub fn find_member_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut CommitteeMember> {
|
||||
self.members.iter_mut().find(|m| m.user_id == user_id)
|
||||
}
|
||||
|
||||
/// Remove a member from the committee
|
||||
pub fn remove_member(&mut self, member_id: u32) -> bool {
|
||||
let len = self.members.len();
|
||||
self.members.retain(|m| m.id != member_id);
|
||||
let removed = self.members.len() < len;
|
||||
|
||||
if removed {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
/// Link this committee to a Circle for access control
|
||||
pub fn link_to_circle(&mut self, circle_id: u32) {
|
||||
self.circle_id = Some(circle_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Get all users who are members of this committee
|
||||
pub fn get_member_users(&self, db: &DB) -> DbResult<Vec<User>> {
|
||||
let mut users = Vec::new();
|
||||
|
||||
for member in &self.members {
|
||||
if let Ok(user) = db.get::<User>(member.user_id) {
|
||||
users.push(user);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Committee {
|
||||
}
|
||||
|
||||
impl Storable for CommitteeMember {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Committee {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"committee"
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
use crate::db::{Model, Storable, DbResult};
|
||||
use crate::db::db::DB;
|
||||
use super::shareholder::Shareholder; // Use super:: for sibling module
|
||||
use super::Resolution;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// CompanyStatus represents the status of a company
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CompanyStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
}
|
||||
|
||||
/// BusinessType represents the type of a business
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BusinessType(pub String);
|
||||
|
||||
impl BusinessType {
|
||||
pub const CORPORATION: &'static str = "Corporation";
|
||||
pub const PARTNERSHIP: &'static str = "Partnership";
|
||||
pub const LLC: &'static str = "LLC";
|
||||
pub const COOP: &'static str = "Coop";
|
||||
pub const SINGLE: &'static str = "Single";
|
||||
pub const TWIN: &'static str = "Twin";
|
||||
pub const STARTER: &'static str = "Starter";
|
||||
pub const GLOBAL: &'static str = "Global";
|
||||
|
||||
/// Create a new BusinessType, validating that the type is one of the predefined types
|
||||
pub fn new(type_str: String) -> Result<Self, String> {
|
||||
if Self::is_valid(&type_str) {
|
||||
Ok(BusinessType(type_str))
|
||||
} else {
|
||||
Err(format!("Invalid business type: {}. Valid types are: {}",
|
||||
type_str, Self::valid_types().join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new BusinessType without validation (use with caution)
|
||||
pub fn new_unchecked(type_str: String) -> Self {
|
||||
BusinessType(type_str)
|
||||
}
|
||||
|
||||
/// Get the string value of the business type
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Check if a string is a valid business type
|
||||
pub fn is_valid(type_str: &str) -> bool {
|
||||
Self::valid_types().contains(&type_str.to_string())
|
||||
}
|
||||
|
||||
/// Get a list of all valid business types
|
||||
pub fn valid_types() -> Vec<String> {
|
||||
vec![
|
||||
Self::CORPORATION.to_string(),
|
||||
Self::PARTNERSHIP.to_string(),
|
||||
Self::LLC.to_string(),
|
||||
Self::COOP.to_string(),
|
||||
Self::SINGLE.to_string(),
|
||||
Self::TWIN.to_string(),
|
||||
Self::STARTER.to_string(),
|
||||
Self::GLOBAL.to_string(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Company represents a company entity
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
|
||||
pub struct Company {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub registration_number: String,
|
||||
pub incorporation_date: DateTime<Utc>,
|
||||
pub fiscal_year_end: String,
|
||||
pub email: String,
|
||||
pub phone: String,
|
||||
pub website: String,
|
||||
pub address: String,
|
||||
pub business_type: BusinessType,
|
||||
pub industry: String,
|
||||
pub description: String,
|
||||
pub status: CompanyStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
// Removed shareholders property
|
||||
}
|
||||
|
||||
impl Storable for Company{}
|
||||
|
||||
// Model requires get_id and db_prefix
|
||||
impl Model for Company {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"company" // Prefix for company records in the database
|
||||
}
|
||||
}
|
||||
|
||||
impl Company {
|
||||
/// Create a new company with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
registration_number: String,
|
||||
incorporation_date: DateTime<Utc>,
|
||||
fiscal_year_end: String,
|
||||
email: String,
|
||||
phone: String,
|
||||
website: String,
|
||||
address: String,
|
||||
business_type: BusinessType,
|
||||
industry: String,
|
||||
description: String,
|
||||
status: CompanyStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
registration_number,
|
||||
incorporation_date,
|
||||
fiscal_year_end,
|
||||
email,
|
||||
phone,
|
||||
website,
|
||||
address,
|
||||
business_type,
|
||||
industry,
|
||||
description,
|
||||
status,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a shareholder to the company, saving it to the database
|
||||
pub fn add_shareholder(
|
||||
&mut self,
|
||||
db: &mut DB, // Pass in the DB instance
|
||||
mut shareholder: Shareholder,
|
||||
) -> DbResult<()> {
|
||||
shareholder.company_id = self.id; // Set the company_id
|
||||
db.set(&shareholder)?; // Insert the shareholder into the DB
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Link this company to a Circle for access control
|
||||
pub fn link_to_circle(&mut self, _circle_id: u32) {
|
||||
// Implementation would involve updating a mapping in a separate database
|
||||
// For now, we'll just update the timestamp to indicate the change
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link this company to a Customer in the biz module
|
||||
pub fn link_to_customer(&mut self, _customer_id: u32) {
|
||||
// Implementation would involve updating a mapping in a separate database
|
||||
// For now, we'll just update the timestamp to indicate the change
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Get all resolutions for this company
|
||||
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<Resolution>> {
|
||||
let all_resolutions = db.list::<Resolution>()?;
|
||||
let company_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.company_id == self.id)
|
||||
.collect();
|
||||
|
||||
Ok(company_resolutions)
|
||||
}
|
||||
|
||||
// Future methods:
|
||||
// /// Get all committees for this company
|
||||
// pub fn get_committees(&self, db: &DB) -> DbResult<Vec<Committee>> { ... }
|
||||
//
|
||||
// /// Get all compliance requirements for this company
|
||||
// pub fn get_compliance_requirements(&self, db: &DB) -> DbResult<Vec<ComplianceRequirement>> { ... }
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DB, DbError, DbResult}; // Import traits from db module
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
|
||||
/// MeetingStatus represents the status of a meeting
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MeetingStatus {
|
||||
Scheduled,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// AttendeeRole represents the role of an attendee in a meeting
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AttendeeRole {
|
||||
Coordinator,
|
||||
Member,
|
||||
Secretary,
|
||||
Participant,
|
||||
Advisor,
|
||||
Admin,
|
||||
}
|
||||
|
||||
/// AttendeeStatus represents the status of an attendee's participation
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AttendeeStatus {
|
||||
Confirmed,
|
||||
Pending,
|
||||
Declined,
|
||||
}
|
||||
|
||||
/// Attendee represents an attendee of a board meeting
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Attendee {
|
||||
pub id: u32,
|
||||
pub meeting_id: u32,
|
||||
pub user_id: u32,
|
||||
pub name: String,
|
||||
pub role: AttendeeRole,
|
||||
pub status: AttendeeStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Attendee {
|
||||
/// Create a new attendee with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
meeting_id: u32,
|
||||
user_id: u32,
|
||||
name: String,
|
||||
role: AttendeeRole,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
meeting_id,
|
||||
user_id,
|
||||
name,
|
||||
role,
|
||||
status: AttendeeStatus::Pending,
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of an attendee
|
||||
pub fn update_status(&mut self, status: AttendeeStatus) {
|
||||
self.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
/// Meeting represents a board meeting of a company or other meeting
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Meeting {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub title: String,
|
||||
pub date: DateTime<Utc>,
|
||||
pub location: String,
|
||||
pub description: String,
|
||||
pub status: MeetingStatus,
|
||||
pub minutes: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub attendees: Vec<Attendee>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
|
||||
impl Meeting {
|
||||
/// Create a new meeting with default values
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
title: String,
|
||||
date: DateTime<Utc>,
|
||||
location: String,
|
||||
description: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
title,
|
||||
date,
|
||||
location,
|
||||
description,
|
||||
status: MeetingStatus::Scheduled,
|
||||
minutes: String::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
attendees: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an attendee to the meeting
|
||||
pub fn add_attendee(&mut self, attendee: Attendee) {
|
||||
// Make sure the attendee's meeting_id matches this meeting
|
||||
assert_eq!(self.id, attendee.meeting_id, "Attendee meeting_id must match meeting id");
|
||||
|
||||
// Check if the attendee already exists
|
||||
if !self.attendees.iter().any(|a| a.id == attendee.id) {
|
||||
self.attendees.push(attendee);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the meeting
|
||||
pub fn update_status(&mut self, status: MeetingStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the meeting minutes
|
||||
pub fn update_minutes(&mut self, minutes: String) {
|
||||
self.minutes = minutes;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Find an attendee by user ID
|
||||
pub fn find_attendee_by_user_id(&self, user_id: u32) -> Option<&Attendee> {
|
||||
self.attendees.iter().find(|a| a.user_id == user_id)
|
||||
}
|
||||
|
||||
/// Find an attendee by user ID (mutable version)
|
||||
pub fn find_attendee_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut Attendee> {
|
||||
self.attendees.iter_mut().find(|a| a.user_id == user_id)
|
||||
}
|
||||
|
||||
/// Get all confirmed attendees
|
||||
pub fn confirmed_attendees(&self) -> Vec<&Attendee> {
|
||||
self.attendees
|
||||
.iter()
|
||||
.filter(|a| a.status == AttendeeStatus::Confirmed)
|
||||
.collect()
|
||||
}
|
||||
/// Link this meeting to a Calendar Event in the mcc module
|
||||
pub fn link_to_event(&mut self, _event_id: u32) -> DbResult<()> {
|
||||
// Implementation would involve updating a mapping in a separate database
|
||||
// For now, we'll just update the timestamp to indicate the change
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all resolutions discussed in this meeting
|
||||
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<super::Resolution>> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
let meeting_resolutions = all_resolutions
|
||||
.into_iter()
|
||||
.filter(|resolution| resolution.meeting_id == Some(self.id))
|
||||
.collect();
|
||||
|
||||
Ok(meeting_resolutions)
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Meeting{}
|
||||
// Implement Model trait
|
||||
impl Model for Meeting {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"meeting"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
pub mod company;
|
||||
pub mod shareholder;
|
||||
pub mod meeting;
|
||||
pub mod user;
|
||||
pub mod vote;
|
||||
pub mod resolution;
|
||||
// All modules:
|
||||
pub mod committee;
|
||||
|
||||
// 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};
|
||||
pub use committee::{Committee, CommitteeMember, CommitteeRole};
|
||||
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};
|
||||
@@ -1,195 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DB, DbError, DbResult};
|
||||
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();
|
||||
}
|
||||
|
||||
/// Withdraw the resolution
|
||||
pub fn withdraw(&mut self) {
|
||||
self.status = ResolutionStatus::Withdrawn;
|
||||
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()
|
||||
}
|
||||
|
||||
/// Find an approval by user ID
|
||||
pub fn find_approval_by_user_id(&self, user_id: u32) -> Option<&Approval> {
|
||||
self.approvals.iter().find(|a| a.user_id == user_id)
|
||||
}
|
||||
|
||||
/// Get all approvals
|
||||
pub fn get_approvals(&self) -> &[Approval] {
|
||||
&self.approvals
|
||||
}
|
||||
|
||||
/// Get approval count
|
||||
pub fn approval_count(&self) -> usize {
|
||||
self.approvals.iter().filter(|a| a.approved).count()
|
||||
}
|
||||
|
||||
/// Get rejection count
|
||||
pub fn rejection_count(&self) -> usize {
|
||||
self.approvals.iter().filter(|a| !a.approved).count()
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// Get the meeting associated with this resolution
|
||||
pub fn get_meeting(&self, db: &DB) -> DbResult<Option<Meeting>> {
|
||||
match self.meeting_id {
|
||||
Some(meeting_id) => {
|
||||
let meeting = db.get::<Meeting>(meeting_id)?;
|
||||
Ok(Some(meeting))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the vote associated with this resolution
|
||||
pub fn get_vote(&self, db: &DB) -> DbResult<Option<Vote>> {
|
||||
match self.vote_id {
|
||||
Some(vote_id) => {
|
||||
let vote = db.get::<Vote>(vote_id)?;
|
||||
Ok(Some(vote))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Storable for Resolution{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Resolution {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"resolution"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::db::{Model, Storable}; // Import db traits
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
|
||||
/// ShareholderType represents the type of shareholder
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ShareholderType {
|
||||
Individual,
|
||||
Corporate,
|
||||
}
|
||||
|
||||
/// Shareholder represents a shareholder of a company
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
|
||||
pub struct Shareholder {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub user_id: u32,
|
||||
pub name: String,
|
||||
pub shares: f64,
|
||||
pub percentage: f64,
|
||||
pub type_: ShareholderType,
|
||||
pub since: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
|
||||
impl Shareholder {
|
||||
/// Create a new shareholder with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
user_id: u32,
|
||||
name: String,
|
||||
shares: f64,
|
||||
percentage: f64,
|
||||
type_: ShareholderType,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
user_id,
|
||||
name,
|
||||
shares,
|
||||
percentage,
|
||||
type_,
|
||||
since: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the shares owned by this shareholder
|
||||
pub fn update_shares(&mut self, shares: f64, percentage: f64) {
|
||||
self.shares = shares;
|
||||
self.percentage = percentage;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Shareholder{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Shareholder {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"shareholder"
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable}; // Import db traits
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
/// User represents a user in the governance system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub company: String, // here its just a best effort
|
||||
pub role: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
|
||||
impl User {
|
||||
/// Create a new user with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
email: String,
|
||||
password: String,
|
||||
company: String,
|
||||
role: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
company,
|
||||
role,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for User{}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for User {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"user"
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::{Model, Storable, DB, DbError, DbResult}; // Import traits from db module
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
|
||||
/// VoteStatus represents the status of a vote
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum VoteStatus {
|
||||
Open,
|
||||
Closed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Vote represents a voting item for corporate decision-making
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Vote {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub status: VoteStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub options: Vec<VoteOption>,
|
||||
pub ballots: Vec<Ballot>,
|
||||
pub private_group: Vec<u32>, // user id's only people who can vote
|
||||
}
|
||||
|
||||
/// VoteOption represents an option in a vote
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VoteOption {
|
||||
pub id: u8,
|
||||
pub vote_id: u32,
|
||||
pub text: String,
|
||||
pub count: i32,
|
||||
pub min_valid: i32, // min votes we need to make total vote count
|
||||
}
|
||||
|
||||
/// The vote as done by the user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Ballot {
|
||||
pub id: u32,
|
||||
pub vote_id: u32,
|
||||
pub user_id: u32,
|
||||
pub vote_option_id: u8,
|
||||
pub shares_count: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Storable for Vote{}
|
||||
|
||||
impl Vote {
|
||||
/// Create a new vote with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
status: VoteStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
title,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
status,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
options: Vec::new(),
|
||||
ballots: Vec::new(),
|
||||
private_group: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a voting option to this vote
|
||||
pub fn add_option(&mut self, text: String, min_valid: i32) -> &VoteOption {
|
||||
let id = if self.options.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.options.iter().map(|o| o.id).max().unwrap_or(0) + 1
|
||||
};
|
||||
|
||||
let option = VoteOption {
|
||||
id,
|
||||
vote_id: self.id,
|
||||
text,
|
||||
count: 0,
|
||||
min_valid,
|
||||
};
|
||||
|
||||
self.options.push(option);
|
||||
self.options.last().unwrap()
|
||||
}
|
||||
|
||||
/// Add a ballot to this vote
|
||||
pub fn add_ballot(&mut self, user_id: u32, vote_option_id: u8, shares_count: i32) -> &Ballot {
|
||||
let id = if self.ballots.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.ballots.iter().map(|b| b.id).max().unwrap_or(0) + 1
|
||||
};
|
||||
|
||||
let ballot = Ballot {
|
||||
id,
|
||||
vote_id: self.id,
|
||||
user_id,
|
||||
vote_option_id,
|
||||
shares_count,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
|
||||
// Update the vote count for the selected option
|
||||
if let Some(option) = self.options.iter_mut().find(|o| o.id == vote_option_id) {
|
||||
option.count += shares_count;
|
||||
}
|
||||
|
||||
self.ballots.push(ballot);
|
||||
self.ballots.last().unwrap()
|
||||
}
|
||||
|
||||
/// Get the resolution associated with this vote
|
||||
pub fn get_resolution(&self, db: &DB) -> DbResult<Option<super::Resolution>> {
|
||||
let all_resolutions = db.list::<super::Resolution>()?;
|
||||
let vote_resolution = all_resolutions
|
||||
.into_iter()
|
||||
.find(|resolution| resolution.vote_id == Some(self.id));
|
||||
|
||||
Ok(vote_resolution)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Vote {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"vote"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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)
|
||||
- create a new rootobject called wallet
|
||||
- has a name, description, blockchainname (string), pubkey
|
||||
- a wallet has embedded struct for asset which is name e.g. USDC and float which is the amount of money in the asset
|
||||
- a member has one or more wallets, in member link to the id's of the wallet
|
||||
|
||||
|
||||
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,96 +0,0 @@
|
||||
# MCC (Mail, Calendar, Contacts) Core Models
|
||||
|
||||
This directory contains the core data structures used in the herolib MCC module. These models serve as the foundation for the mail, calendar, and contacts functionality.
|
||||
|
||||
## Overview
|
||||
|
||||
The core models implement the Serde traits (Serialize/Deserialize) and crate database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
|
||||
|
||||
- A struct definition with appropriate fields
|
||||
- Serde serialization through derive macros
|
||||
- Methods for database integration through the SledModel trait
|
||||
- Utility methods for common operations
|
||||
|
||||
## Core Models
|
||||
|
||||
### Mail (`mail.rs`)
|
||||
|
||||
The Mail models provide email and IMAP functionality:
|
||||
|
||||
- **Email**: Main struct for email messages with IMAP metadata
|
||||
- **Attachment**: Represents a file attachment with file information
|
||||
- **Envelope**: Represents an IMAP envelope structure with message headers
|
||||
|
||||
### Message (`message.rs`)
|
||||
|
||||
The Message models provide chat functionality:
|
||||
|
||||
- **Message**: Main struct for chat messages with thread and recipient information
|
||||
- **MessageMeta**: Contains metadata for message status, editing, and reactions
|
||||
- **MessageStatus**: Enum representing the status of a message (Sent, Delivered, Read, Failed)
|
||||
|
||||
### Calendar (`calendar.rs`)
|
||||
|
||||
The Calendar model represents a container for calendar events:
|
||||
|
||||
- **Calendar**: Main struct with fields for identification and description
|
||||
|
||||
### Event (`event.rs`)
|
||||
|
||||
The Event model provides calendar event management:
|
||||
|
||||
- **Event**: Main struct for calendar events with time and attendee information
|
||||
- **EventMeta**: Contains additional metadata for synchronization and display
|
||||
|
||||
### Contacts (`contacts.rs`)
|
||||
|
||||
The Contacts model provides contact management:
|
||||
|
||||
- **Contact**: Main struct for contact information with personal details and grouping
|
||||
|
||||
## Group Support
|
||||
|
||||
All models now support linking to multiple groups (Circle IDs):
|
||||
|
||||
- Each model has a `groups: Vec<u32>` field to store multiple group IDs
|
||||
- Utility methods for adding, removing, and filtering by groups
|
||||
- Groups are defined in the Circle module
|
||||
|
||||
## Utility Methods
|
||||
|
||||
Each model provides utility methods for:
|
||||
|
||||
### Filtering/Searching
|
||||
- `filter_by_groups(groups: &[u32]) -> bool`: Filter by groups
|
||||
- `search_by_subject/content/name/email(query: &str) -> bool`: Search by various fields
|
||||
|
||||
### Format Conversion
|
||||
- `to_message()`: Convert Email to Message
|
||||
|
||||
### Relationship Management
|
||||
- `get_events()`: Get events associated with a calendar or contact
|
||||
- `get_calendar()`: Get the calendar an event belongs to
|
||||
- `get_attendee_contacts()`: Get contacts for event attendees
|
||||
- `get_thread_messages()`: Get all messages in the same thread
|
||||
|
||||
## Usage
|
||||
|
||||
These models are used by the MCC module to manage emails, calendar events, and contacts. They are typically accessed through the database handlers that implement the generic SledDB interface.
|
||||
|
||||
## Serialization
|
||||
|
||||
All models use Serde for serialization:
|
||||
|
||||
- Each model implements Serialize and Deserialize traits through derive macros
|
||||
- Binary serialization is handled automatically by the database layer
|
||||
- JSON serialization is available for API responses and other use cases
|
||||
|
||||
## Database Integration
|
||||
|
||||
The models are designed to work with the SledDB implementation through:
|
||||
|
||||
- The `Storable` trait for serialization/deserialization
|
||||
- The `SledModel` trait for database operations:
|
||||
- `get_id()` method for unique identification
|
||||
- `db_prefix()` method to specify the collection prefix
|
||||
- Implementation of custom utility methods where needed
|
||||
@@ -1,56 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::mcc::event::Event;
|
||||
use crate::db::model::impl_get_id;
|
||||
|
||||
/// Calendar represents a calendar container for events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Calendar {
|
||||
pub id: u32, // Unique identifier
|
||||
pub title: String, // Calendar title
|
||||
pub description: String, // Calendar details
|
||||
pub groups: Vec<u32>, // Groups this calendar belongs to (references Circle IDs)
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
/// Create a new calendar
|
||||
pub fn new(id: u32, title: String, description: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a group to this calendar
|
||||
pub fn add_group(&mut self, group_id: u32) {
|
||||
if !self.groups.contains(&group_id) {
|
||||
self.groups.push(group_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a group from this calendar
|
||||
pub fn remove_group(&mut self, group_id: u32) {
|
||||
self.groups.retain(|&id| id != group_id);
|
||||
}
|
||||
|
||||
/// Filter by groups - returns true if this calendar belongs to any of the specified groups
|
||||
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
|
||||
groups.iter().any(|g| self.groups.contains(g))
|
||||
}
|
||||
|
||||
/// Filter events by this calendar's ID
|
||||
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
|
||||
events.iter()
|
||||
.filter(|event| event.calendar_id == self.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the database prefix for this model type
|
||||
pub fn db_prefix() -> &'static str {
|
||||
"calendar"
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically implement GetId trait for Calendar
|
||||
impl_get_id!(Calendar);
|
||||
@@ -1,90 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::mcc::event::Event;
|
||||
use crate::db::model::impl_get_id;
|
||||
use chrono::Utc;
|
||||
|
||||
/// Contact represents a contact entry in an address book
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Contact {
|
||||
// Database ID
|
||||
pub id: u32, // Database ID (assigned by DBHandler)
|
||||
// Content fields
|
||||
pub created_at: i64, // Unix epoch timestamp
|
||||
pub modified_at: i64, // Unix epoch timestamp
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub emails: Vec<String>, // Changed from []String to Vec<String>
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
/// Create a new contact
|
||||
pub fn new(id: u32, first_name: String, last_name: String, emails: Vec<String>) -> Self {
|
||||
let now = Utc::now().timestamp();
|
||||
Self {
|
||||
id,
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
first_name,
|
||||
last_name,
|
||||
emails : emails,
|
||||
}
|
||||
}
|
||||
|
||||
/// Search by name - returns true if the name contains the query (case-insensitive)
|
||||
pub fn search_by_name(&self, query: &str) -> bool {
|
||||
let full_name = self.full_name().to_lowercase();
|
||||
query.to_lowercase().split_whitespace().all(|word| full_name.contains(word))
|
||||
}
|
||||
|
||||
/// Search by email - returns true if the email contains the query (case-insensitive)
|
||||
pub fn search_by_email(&self, query: &str) -> bool {
|
||||
self.email.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
/// Filter events where this contact is an attendee
|
||||
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
|
||||
events.iter()
|
||||
.filter(|event| event.attendees.contains(&self.email))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update the contact's information
|
||||
pub fn update(&mut self, first_name: Option<String>, last_name: Option<String>, email: Option<String>, group: Option<String>) {
|
||||
if let Some(first_name) = first_name {
|
||||
self.first_name = first_name;
|
||||
}
|
||||
|
||||
if let Some(last_name) = last_name {
|
||||
self.last_name = last_name;
|
||||
}
|
||||
|
||||
if let Some(email) = email {
|
||||
self.email = email;
|
||||
}
|
||||
|
||||
if let Some(group) = group {
|
||||
self.group = group;
|
||||
}
|
||||
|
||||
self.modified_at = Utc::now().timestamp();
|
||||
}
|
||||
|
||||
/// Update the contact's groups
|
||||
pub fn update_groups(&mut self, groups: Vec<u32>) {
|
||||
self.groups = groups;
|
||||
self.modified_at = Utc::now().timestamp();
|
||||
}
|
||||
|
||||
/// Get the full name of the contact
|
||||
pub fn full_name(&self) -> String {
|
||||
format!("{} {}", self.first_name, self.last_name)
|
||||
}
|
||||
|
||||
/// Get the database prefix for this model type
|
||||
pub fn db_prefix() -> &'static str {
|
||||
"contact"
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically implement GetId trait for Contact
|
||||
impl_get_id!(Contact);
|
||||
@@ -1,131 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::models::mcc::calendar::Calendar;
|
||||
use crate::models::mcc::contacts::Contact;
|
||||
use crate::db::model::impl_get_id;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// EventMeta contains additional metadata for a calendar event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EventMeta {
|
||||
pub caldav_uid: String, // CalDAV UID for syncing
|
||||
pub sync_token: String, // Sync token for tracking changes
|
||||
pub etag: String, // ETag for caching
|
||||
pub color: String, // User-friendly color categorization
|
||||
}
|
||||
|
||||
/// Represents a calendar event with all its properties
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
pub id: u32, // Unique identifier
|
||||
pub calendar_id: u32, // ID of the calendar this event belongs to
|
||||
pub title: String, // Event title
|
||||
pub description: String, // Event details
|
||||
pub location: String, // Event location
|
||||
pub start_time: DateTime<Utc>, // Start time
|
||||
pub end_time: DateTime<Utc>, // End time
|
||||
pub all_day: bool, // True if it's an all-day event
|
||||
pub recurrence: String, // RFC 5545 Recurrence Rule (e.g., "FREQ=DAILY;COUNT=10")
|
||||
pub attendees: Vec<String>, // List of emails or user IDs
|
||||
pub organizer: String, // Organizer email
|
||||
pub status: String, // "CONFIRMED", "CANCELLED", "TENTATIVE"
|
||||
pub meta: EventMeta, // Additional metadata
|
||||
pub groups: Vec<u32>, // Groups this event belongs to (references Circle IDs)
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Create a new event
|
||||
pub fn new(
|
||||
id: u32,
|
||||
calendar_id: u32,
|
||||
title: String,
|
||||
description: String,
|
||||
location: String,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
organizer: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
calendar_id,
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
start_time,
|
||||
end_time,
|
||||
all_day: false,
|
||||
recurrence: String::new(),
|
||||
attendees: Vec::new(),
|
||||
organizer,
|
||||
status: "CONFIRMED".to_string(),
|
||||
meta: EventMeta {
|
||||
caldav_uid: String::new(),
|
||||
sync_token: String::new(),
|
||||
etag: String::new(),
|
||||
color: String::new(),
|
||||
},
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a group to this event
|
||||
pub fn add_group(&mut self, group_id: u32) {
|
||||
if !self.groups.contains(&group_id) {
|
||||
self.groups.push(group_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a group from this event
|
||||
pub fn remove_group(&mut self, group_id: u32) {
|
||||
self.groups.retain(|&id| id != group_id);
|
||||
}
|
||||
|
||||
/// Filter by groups - returns true if this event belongs to any of the specified groups
|
||||
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
|
||||
groups.iter().any(|g| self.groups.contains(g))
|
||||
}
|
||||
|
||||
/// Find the calendar this event belongs to
|
||||
pub fn find_calendar<'a>(&self, calendars: &'a [Calendar]) -> Option<&'a Calendar> {
|
||||
calendars.iter().find(|cal| cal.id == self.calendar_id)
|
||||
}
|
||||
|
||||
/// Filter contacts that are attendees of this event
|
||||
pub fn filter_attendee_contacts<'a>(&self, contacts: &'a [Contact]) -> Vec<&'a Contact> {
|
||||
contacts.iter()
|
||||
.filter(|contact| self.attendees.contains(&contact.email))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Add an attendee to this event
|
||||
pub fn add_attendee(&mut self, attendee: String) {
|
||||
self.attendees.push(attendee);
|
||||
}
|
||||
|
||||
/// Set event to all day
|
||||
pub fn set_all_day(&mut self, all_day: bool) {
|
||||
self.all_day = all_day;
|
||||
}
|
||||
|
||||
/// Set event status
|
||||
pub fn set_status(&mut self, status: &str) {
|
||||
self.status = status.to_string();
|
||||
}
|
||||
|
||||
/// Search by title - returns true if the title contains the query (case-insensitive)
|
||||
pub fn search_by_title(&self, query: &str) -> bool {
|
||||
self.title.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
/// Search by description - returns true if the description contains the query (case-insensitive)
|
||||
pub fn search_by_description(&self, query: &str) -> bool {
|
||||
self.description.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
/// Get the database prefix for this model type
|
||||
pub fn db_prefix() -> &'static str {
|
||||
"event"
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically implement GetId trait for Event
|
||||
impl_get_id!(Event);
|
||||
@@ -1,12 +0,0 @@
|
||||
pub mod calendar;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod contacts;
|
||||
pub mod message;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use calendar::Calendar;
|
||||
pub use event::{Event, EventMeta};
|
||||
pub use mail::{Email, Attachment, Envelope};
|
||||
pub use contacts::Contact;
|
||||
pub use message::{Message, MessageMeta, MessageStatus};
|
||||
@@ -1,129 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::model::impl_get_id;
|
||||
use chrono::Utc;
|
||||
|
||||
/// Email represents an email message with all its metadata and content
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Email {
|
||||
// Database ID
|
||||
pub id: u32,
|
||||
pub message: String, // The email body content
|
||||
pub attachments: Vec<Attachment>, // Any file attachments
|
||||
pub flags: Vec<String>, // IMAP flags like \Seen, \Deleted, etc.
|
||||
pub receivetime: i64, // Unix timestamp when the email was received
|
||||
pub envelope: Option<Envelope>, // IMAP envelope structure
|
||||
}
|
||||
|
||||
/// Attachment represents an email attachment
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Attachment {
|
||||
pub filename: String,
|
||||
pub content_type: String,
|
||||
pub hash: String, // In each circle we have unique dedupe DB, this is the hash of the fileobject
|
||||
pub size: u32, // Size in kb of the attachment
|
||||
}
|
||||
|
||||
/// Envelope represents an IMAP envelope structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Envelope {
|
||||
pub date: i64,
|
||||
pub subject: String,
|
||||
pub from: Vec<String>,
|
||||
pub sender: Vec<String>,
|
||||
pub reply_to: Vec<String>,
|
||||
pub to: Vec<String>,
|
||||
pub cc: Vec<String>,
|
||||
pub bcc: Vec<String>,
|
||||
pub in_reply_to: String,
|
||||
}
|
||||
|
||||
impl Email {
|
||||
/// Create a new email
|
||||
pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
message,
|
||||
attachments: Vec::new(),
|
||||
flags: Vec::new(),
|
||||
receivetime: chrono::Utc::now().timestamp(),
|
||||
envelope: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an attachment to this email
|
||||
pub fn add_attachment(&mut self, attachment: Attachment) {
|
||||
self.attachments.push(attachment);
|
||||
}
|
||||
|
||||
/// Search by subject - returns true if the subject contains the query (case-insensitive)
|
||||
pub fn search_by_subject(&self, query: &str) -> bool {
|
||||
if let Some(env) = &self.envelope {
|
||||
env.subject.to_lowercase().contains(&query.to_lowercase())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Search by content - returns true if the message content contains the query (case-insensitive)
|
||||
pub fn search_by_content(&self, query: &str) -> bool {
|
||||
self.message.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
/// Set the envelope for this email
|
||||
pub fn set_envelope(&mut self, envelope: Envelope) {
|
||||
self.envelope = Some(envelope);
|
||||
}
|
||||
|
||||
/// Convert this email to a Message (for chat)
|
||||
pub fn to_message(&self, id: u32, thread_id: String) -> crate::models::mcc::message::Message {
|
||||
use crate::models::mcc::message::Message;
|
||||
|
||||
let _now = Utc::now();
|
||||
let sender = if let Some(env) = &self.envelope {
|
||||
if !env.from.is_empty() {
|
||||
env.from[0].clone()
|
||||
} else {
|
||||
"unknown@example.com".to_string()
|
||||
}
|
||||
} else {
|
||||
"unknown@example.com".to_string()
|
||||
};
|
||||
|
||||
let subject = if let Some(env) = &self.envelope {
|
||||
env.subject.clone()
|
||||
} else {
|
||||
"No Subject".to_string()
|
||||
};
|
||||
|
||||
let recipients = if let Some(env) = &self.envelope {
|
||||
env.to.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let content = if !subject.is_empty() {
|
||||
format!("{}\n\n{}", subject, self.message)
|
||||
} else {
|
||||
self.message.clone()
|
||||
};
|
||||
|
||||
let mut message = Message::new(id, thread_id, sender, content);
|
||||
message.recipients = recipients;
|
||||
message.groups = self.groups.clone();
|
||||
|
||||
// Convert attachments to references
|
||||
for attachment in &self.attachments {
|
||||
message.add_attachment(attachment.filename.clone());
|
||||
}
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
/// Get the database prefix for this model type
|
||||
pub fn db_prefix() -> &'static str {
|
||||
"email"
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically implement GetId trait for Email
|
||||
impl_get_id!(Email);
|
||||
@@ -1,122 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::impl_get_id;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// MessageStatus represents the status of a message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MessageStatus {
|
||||
Sent,
|
||||
Delivered,
|
||||
Read,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// MessageMeta contains metadata for a chat message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageMeta {
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub status: MessageStatus,
|
||||
pub is_edited: bool,
|
||||
pub reactions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Message represents a chat message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub id: u32, // Unique identifier
|
||||
pub thread_id: String, // Thread/conversation identifier
|
||||
pub sender_id: String, // Sender identifier
|
||||
pub recipients: Vec<String>, // List of recipient identifiers
|
||||
pub content: String, // Message content
|
||||
pub attachments: Vec<String>, // References to attachments
|
||||
pub meta: MessageMeta, // Message metadata
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Create a new message
|
||||
pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
thread_id,
|
||||
sender_id,
|
||||
recipients: Vec::new(),
|
||||
content,
|
||||
attachments: Vec::new(),
|
||||
meta: MessageMeta {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
status: MessageStatus::Sent,
|
||||
is_edited: false,
|
||||
reactions: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a recipient to this message
|
||||
pub fn add_recipient(&mut self, recipient: String) {
|
||||
self.recipients.push(recipient);
|
||||
}
|
||||
|
||||
/// Add an attachment to this message
|
||||
pub fn add_attachment(&mut self, attachment: String) {
|
||||
self.attachments.push(attachment);
|
||||
}
|
||||
|
||||
/// Add a group to this message
|
||||
pub fn add_group(&mut self, group_id: u32) {
|
||||
if !self.groups.contains(&group_id) {
|
||||
self.groups.push(group_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a group from this message
|
||||
pub fn remove_group(&mut self, group_id: u32) {
|
||||
self.groups.retain(|&id| id != group_id);
|
||||
}
|
||||
|
||||
/// Filter by groups - returns true if this message belongs to any of the specified groups
|
||||
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
|
||||
groups.iter().any(|g| self.groups.contains(g))
|
||||
}
|
||||
|
||||
/// Search by content - returns true if the content contains the query (case-insensitive)
|
||||
pub fn search_by_content(&self, query: &str) -> bool {
|
||||
self.content.to_lowercase().contains(&query.to_lowercase())
|
||||
}
|
||||
|
||||
/// Update message status
|
||||
pub fn update_status(&mut self, status: MessageStatus) {
|
||||
self.meta.status = status;
|
||||
self.meta.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Edit message content
|
||||
pub fn edit_content(&mut self, new_content: String) {
|
||||
self.content = new_content;
|
||||
self.meta.is_edited = true;
|
||||
self.meta.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Add a reaction to the message
|
||||
pub fn add_reaction(&mut self, reaction: String) {
|
||||
self.meta.reactions.push(reaction);
|
||||
self.meta.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Filter messages that are in the same thread as this message
|
||||
pub fn filter_thread_messages<'a>(&self, messages: &'a [Message]) -> Vec<&'a Message> {
|
||||
messages.iter()
|
||||
.filter(|msg| msg.thread_id == self.thread_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the database prefix for this model type
|
||||
pub fn db_prefix() -> &'static str {
|
||||
"message"
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically implement GetId trait for Message
|
||||
impl_get_id!(Message);
|
||||
@@ -1,12 +0,0 @@
|
||||
pub mod calendar;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod contacts;
|
||||
pub mod message;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use calendar::Calendar;
|
||||
pub use event::{Event, EventMeta};
|
||||
pub use mail::{Email, Attachment, Envelope};
|
||||
pub use contacts::Contact;
|
||||
pub use message::{Message, MessageMeta, MessageStatus};
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod biz;
|
||||
pub mod mcc;
|
||||
pub mod circle;
|
||||
pub mod gov;
|
||||
@@ -1,131 +0,0 @@
|
||||
# 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
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
Python port of the business models from Rust.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,455 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI server providing CRUD operations for business models.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import (
|
||||
Currency,
|
||||
Customer,
|
||||
Product,
|
||||
ProductComponent,
|
||||
ProductStatus,
|
||||
ProductType,
|
||||
Sale,
|
||||
SaleItem,
|
||||
SaleStatus,
|
||||
)
|
||||
|
||||
# Create database
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
|
||||
# Create tables
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Business API",
|
||||
description="API for business models",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependency to get database session
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to the Business API"}
|
||||
|
||||
|
||||
# Currency endpoints
|
||||
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
|
||||
def create_currency(currency: Currency, session: Session = Depends(get_session)):
|
||||
"""Create a new currency"""
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
|
||||
def read_currencies(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all currencies"""
|
||||
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
|
||||
return currencies
|
||||
|
||||
|
||||
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def read_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a currency by ID"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
return currency
|
||||
|
||||
|
||||
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def update_currency(
|
||||
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
# Update currency attributes
|
||||
currency_data_dict = currency_data.dict(exclude_unset=True)
|
||||
for key, value in currency_data_dict.items():
|
||||
setattr(currency, key, value)
|
||||
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
|
||||
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
session.delete(currency)
|
||||
session.commit()
|
||||
return {"message": "Currency deleted successfully"}
|
||||
|
||||
|
||||
# Customer endpoints
|
||||
@app.post("/customers/", response_model=Customer, tags=["Customers"])
|
||||
def create_customer(customer: Customer, session: Session = Depends(get_session)):
|
||||
"""Create a new customer"""
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
|
||||
def read_customers(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all customers"""
|
||||
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
|
||||
return customers
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def read_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a customer by ID"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def update_customer(
|
||||
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Update customer attributes
|
||||
customer_data_dict = customer_data.dict(exclude_unset=True)
|
||||
for key, value in customer_data_dict.items():
|
||||
setattr(customer, key, value)
|
||||
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.delete("/customers/{customer_id}", tags=["Customers"])
|
||||
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.delete(customer)
|
||||
session.commit()
|
||||
return {"message": "Customer deleted successfully"}
|
||||
|
||||
|
||||
# Product endpoints
|
||||
@app.post("/products/", response_model=Product, tags=["Products"])
|
||||
def create_product(product: Product, session: Session = Depends(get_session)):
|
||||
"""Create a new product"""
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.get("/products/", response_model=List[Product], tags=["Products"])
|
||||
def read_products(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category: Optional[str] = None,
|
||||
status: Optional[ProductStatus] = None,
|
||||
istemplate: Optional[bool] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all products with optional filtering"""
|
||||
query = select(Product)
|
||||
|
||||
if category:
|
||||
query = query.where(Product.category == category)
|
||||
|
||||
if status:
|
||||
query = query.where(Product.status == status)
|
||||
|
||||
if istemplate is not None:
|
||||
query = query.where(Product.istemplate == istemplate)
|
||||
|
||||
products = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def read_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a product by ID"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return product
|
||||
|
||||
|
||||
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def update_product(
|
||||
product_id: int, product_data: Product, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# Update product attributes
|
||||
product_data_dict = product_data.dict(exclude_unset=True)
|
||||
for key, value in product_data_dict.items():
|
||||
setattr(product, key, value)
|
||||
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.delete("/products/{product_id}", tags=["Products"])
|
||||
def delete_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
return {"message": "Product deleted successfully"}
|
||||
|
||||
|
||||
# Product Component endpoints
|
||||
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
|
||||
def create_product_component(
|
||||
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add a component to a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
component.product_id = product_id
|
||||
session.add(component)
|
||||
session.commit()
|
||||
session.refresh(component)
|
||||
return component
|
||||
|
||||
|
||||
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
|
||||
def read_product_components(
|
||||
product_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all components for a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
return product.components
|
||||
|
||||
|
||||
# Sale endpoints
|
||||
@app.post("/sales/", response_model=Sale, tags=["Sales"])
|
||||
def create_sale(sale: Sale, session: Session = Depends(get_session)):
|
||||
"""Create a new sale"""
|
||||
# Ensure customer exists if customer_id is provided
|
||||
if sale.customer_id:
|
||||
customer = session.get(Customer, sale.customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
|
||||
def read_sales(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[SaleStatus] = None,
|
||||
customer_id: Optional[int] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales with optional filtering"""
|
||||
query = select(Sale)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
if customer_id:
|
||||
query = query.where(Sale.customer_id == customer_id)
|
||||
|
||||
sales = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return sales
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def read_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a sale by ID"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
return sale
|
||||
|
||||
|
||||
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def update_sale(
|
||||
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
# Update sale attributes
|
||||
sale_data_dict = sale_data.dict(exclude_unset=True)
|
||||
for key, value in sale_data_dict.items():
|
||||
setattr(sale, key, value)
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.delete("/sales/{sale_id}", tags=["Sales"])
|
||||
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
session.delete(sale)
|
||||
session.commit()
|
||||
return {"message": "Sale deleted successfully"}
|
||||
|
||||
|
||||
# Sale Item endpoints
|
||||
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
|
||||
def create_sale_item(
|
||||
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add an item to a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
item.sale_id = sale_id
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
# Update the sale's total amount
|
||||
sale.add_item(item)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
|
||||
return item
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
|
||||
def read_sale_items(
|
||||
sale_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all items for a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
return sale.items
|
||||
|
||||
|
||||
# Convenience endpoints
|
||||
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
|
||||
def update_sale_status(
|
||||
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update the status of a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
sale.update_status(status)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
|
||||
def get_available_products(
|
||||
istemplate: Optional[bool] = False,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all available products"""
|
||||
query = select(Product).where(
|
||||
Product.status == ProductStatus.AVAILABLE,
|
||||
Product.purchase_till > datetime.utcnow(),
|
||||
Product.istemplate == istemplate
|
||||
)
|
||||
products = session.exec(query).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
|
||||
def get_customer_sales(
|
||||
customer_id: int,
|
||||
status: Optional[SaleStatus] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales for a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
query = select(Sale).where(Sale.customer_id == customer_id)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
sales = session.exec(query).all()
|
||||
return sales
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Binary file not shown.
@@ -1,190 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/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!"
|
||||
@@ -1,297 +0,0 @@
|
||||
"""
|
||||
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()
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,377 +0,0 @@
|
||||
use rhai::{Engine, EvalAltResult, Dynamic, Map};
|
||||
use std::sync::Arc;
|
||||
use std::fs;
|
||||
use crate::core::{DB, SledDBResult};
|
||||
use crate::zaz::factory::create_zaz_db;
|
||||
use crate::zaz::models::{Company, User, Shareholder};
|
||||
|
||||
/// A wrapper around the Rhai Engine that provides additional functionality
|
||||
pub struct RhaiEngine {
|
||||
engine: Engine,
|
||||
}
|
||||
|
||||
impl RhaiEngine {
|
||||
/// Creates a new Rhai engine with all model functions registered
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
rhai_engine
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying Rhai engine
|
||||
pub fn get_engine(&mut self) -> &mut Engine {
|
||||
&mut self.engine
|
||||
}
|
||||
|
||||
/// Register a named function with the engine
|
||||
///
|
||||
/// This allows adding wrapped functions to the engine in a generic way,
|
||||
/// so they can be called from Rhai scripts
|
||||
pub fn register_fn<F>(&mut self, name: &str, f: F) {
|
||||
|
||||
|
||||
//in script engine
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Run a Rhai script from a file
|
||||
///
|
||||
/// This method reads the specified script file and executes it
|
||||
/// using the current engine instance.
|
||||
pub fn run_script(&mut self, file_path: &str) -> Result<(), String> {
|
||||
// Read the script file content
|
||||
let script = match fs::read_to_string(file_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => return Err(format!("Failed to read script file: {}", e))
|
||||
};
|
||||
|
||||
// Execute the script
|
||||
match self.engine.eval::<()>(&script) {
|
||||
Ok(_) => {
|
||||
println!("Rhai script executed successfully: {}", file_path);
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
Err(format!("Rhai script execution failed: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a Rhai script from a text string
|
||||
///
|
||||
/// This method executes the provided script text using the current engine instance.
|
||||
pub fn run(&mut self, script_text: &str) -> Result<(), String> {
|
||||
// Execute the script
|
||||
match self.engine.eval::<()>(script_text) {
|
||||
Ok(_) => {
|
||||
println!("Rhai script text executed successfully");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
Err(format!("Rhai script execution failed: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Rhai engine with all model functions registered
|
||||
/// This function is maintained for backward compatibility
|
||||
pub fn new() -> Engine {
|
||||
let rhai_engine = RhaiEngine::new();
|
||||
rhai_engine.engine
|
||||
}
|
||||
|
||||
/// Register a named function with the engine
|
||||
/// This function is maintained for backward compatibility
|
||||
pub fn register_named_fn<F>(engine: &mut Engine, name: &str, f: F)
|
||||
where
|
||||
F: 'static + Fn(&mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
{
|
||||
match f(engine) {
|
||||
Ok(_) => println!("Function '{}' registered successfully", name),
|
||||
Err(e) => eprintln!("Failed to register function '{}': {}", name, e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a Rhai script from a file
|
||||
/// This function is maintained for backward compatibility
|
||||
pub fn run_script(file_path: &str) -> Result<(), String> {
|
||||
let mut rhai_engine = RhaiEngine::new();
|
||||
rhai_engine.run_script(file_path)
|
||||
}
|
||||
|
||||
/// Run a Rhai script from a text string
|
||||
/// This function is maintained for backward compatibility
|
||||
pub fn run(script_text: &str) -> Result<(), String> {
|
||||
let mut rhai_engine = RhaiEngine::new();
|
||||
rhai_engine.run(script_text)
|
||||
}
|
||||
|
||||
/// Utility functions to safely convert between Rhai Dynamic types and Rust types
|
||||
impl RhaiEngine {
|
||||
/// Helper function to convert a value to a Rhai Dynamic type with proper error handling
|
||||
pub fn to_dynamic<T: Clone + Into<Dynamic>>(&self, value: T) -> Dynamic {
|
||||
value.into()
|
||||
}
|
||||
|
||||
/// Helper function to safely get a property from a map with proper error handling
|
||||
pub fn get_property<T: Clone + 'static>(&self, map: &Map, key: &str) -> Result<T, String>
|
||||
where Dynamic: TryInto<T> {
|
||||
match map.get(key) {
|
||||
Some(value) => {
|
||||
match value.clone().try_into() {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(format!("Property '{}' has wrong type", key))
|
||||
}
|
||||
},
|
||||
None => Err(format!("Property '{}' not found", key))
|
||||
}
|
||||
}
|
||||
|
||||
/// Register CRUD operations for Company
|
||||
pub fn register_company_crud(&mut self) {
|
||||
// Create company from a map of properties
|
||||
self.engine.register_result_fn("create_company_from_map",
|
||||
|map: Map| -> Result<Company, Box<EvalAltResult>> {
|
||||
// Extract required fields with proper error handling
|
||||
let name = match map.get("name") {
|
||||
Some(val) => match val.clone().try_into::<String>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("'name' must be a string".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'name'".into())
|
||||
};
|
||||
|
||||
let reg_number = match map.get("registration_number") {
|
||||
Some(val) => match val.clone().try_into::<String>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("'registration_number' must be a string".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'registration_number'".into())
|
||||
};
|
||||
|
||||
// Extract other fields (example with default values)
|
||||
let id = match map.get("id") {
|
||||
Some(val) => match val.clone().try_into::<i64>() {
|
||||
Ok(id) => id as u32,
|
||||
Err(_) => return Err("'id' must be an integer".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'id'".into())
|
||||
};
|
||||
|
||||
// Here you would call your actual company creation logic
|
||||
// This is just a stub that would be replaced with actual implementation
|
||||
Err("Not fully implemented: would connect to database here".into())
|
||||
}
|
||||
);
|
||||
|
||||
// Read company by ID
|
||||
self.engine.register_result_fn("get_company_by_id",
|
||||
|id: i64| -> Result<Company, Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid company ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Here you would query the database for the company
|
||||
// This is just a stub that would be replaced with actual implementation
|
||||
Err(format!("Company with ID {} not found", id).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Update company
|
||||
self.engine.register_result_fn("update_company",
|
||||
|company: Company| -> Result<(), Box<EvalAltResult>> {
|
||||
// Here you would update the company in the database
|
||||
// This is just a stub that would be replaced with actual implementation
|
||||
Err(format!("Failed to update company: {}", company.name).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Delete company
|
||||
self.engine.register_result_fn("delete_company",
|
||||
|id: i64| -> Result<(), Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid company ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Here you would delete the company from the database
|
||||
// This is just a stub that would be replaced with actual implementation
|
||||
Err(format!("Failed to delete company with ID {}", id).into())
|
||||
}
|
||||
);
|
||||
|
||||
// List companies with filtering capabilities
|
||||
self.engine.register_result_fn("list_companies",
|
||||
|filter: Map| -> Result<Vec<Company>, Box<EvalAltResult>> {
|
||||
// Here you would query the database with the filter
|
||||
// This is just a stub that would be replaced with actual implementation
|
||||
Err("No companies found matching filter".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Register CRUD operations for User
|
||||
pub fn register_user_crud(&mut self) {
|
||||
// Create user from a map of properties
|
||||
self.engine.register_result_fn("create_user_from_map",
|
||||
|map: Map| -> Result<User, Box<EvalAltResult>> {
|
||||
// Extract required fields with proper error handling
|
||||
let name = match map.get("name") {
|
||||
Some(val) => match val.clone().try_into::<String>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("'name' must be a string".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'name'".into())
|
||||
};
|
||||
|
||||
let email = match map.get("email") {
|
||||
Some(val) => match val.clone().try_into::<String>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("'email' must be a string".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'email'".into())
|
||||
};
|
||||
|
||||
// Extract other fields
|
||||
let id = match map.get("id") {
|
||||
Some(val) => match val.clone().try_into::<i64>() {
|
||||
Ok(id) => id as u32,
|
||||
Err(_) => return Err("'id' must be an integer".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'id'".into())
|
||||
};
|
||||
|
||||
// This is a stub that would be replaced with actual implementation
|
||||
Err("Not fully implemented: would create user in database".into())
|
||||
}
|
||||
);
|
||||
|
||||
// Read user by ID
|
||||
self.engine.register_result_fn("get_user_by_id",
|
||||
|id: i64| -> Result<User, Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid user ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Implementation would query database
|
||||
Err(format!("User with ID {} not found", id).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Update user
|
||||
self.engine.register_result_fn("update_user",
|
||||
|user: User| -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation would update the user in the database
|
||||
Err(format!("Failed to update user: {}", user.name).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Delete user
|
||||
self.engine.register_result_fn("delete_user",
|
||||
|id: i64| -> Result<(), Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid user ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Implementation would delete from database
|
||||
Err(format!("Failed to delete user with ID {}", id).into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Register CRUD operations for Shareholder
|
||||
pub fn register_shareholder_crud(&mut self) {
|
||||
// Create shareholder from a map of properties
|
||||
self.engine.register_result_fn("create_shareholder_from_map",
|
||||
|map: Map| -> Result<Shareholder, Box<EvalAltResult>> {
|
||||
// Extract required fields with proper error handling
|
||||
let name = match map.get("name") {
|
||||
Some(val) => match val.clone().try_into::<String>() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("'name' must be a string".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'name'".into())
|
||||
};
|
||||
|
||||
let company_id = match map.get("company_id") {
|
||||
Some(val) => match val.clone().try_into::<i64>() {
|
||||
Ok(id) => id as u32,
|
||||
Err(_) => return Err("'company_id' must be an integer".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'company_id'".into())
|
||||
};
|
||||
|
||||
let shares = match map.get("shares") {
|
||||
Some(val) => match val.clone().try_into::<i64>() {
|
||||
Ok(num) => num,
|
||||
Err(_) => return Err("'shares' must be an integer".into())
|
||||
},
|
||||
None => return Err("Missing required field: 'shares'".into())
|
||||
};
|
||||
|
||||
// This is a stub that would be replaced with actual implementation
|
||||
Err("Not fully implemented: would create shareholder in database".into())
|
||||
}
|
||||
);
|
||||
|
||||
// Read shareholder by ID
|
||||
self.engine.register_result_fn("get_shareholder_by_id",
|
||||
|id: i64| -> Result<Shareholder, Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid shareholder ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Implementation would query database
|
||||
Err(format!("Shareholder with ID {} not found", id).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Update shareholder
|
||||
self.engine.register_result_fn("update_shareholder",
|
||||
|shareholder: Shareholder| -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation would update the shareholder in the database
|
||||
Err(format!("Failed to update shareholder ID: {}", shareholder.id).into())
|
||||
}
|
||||
);
|
||||
|
||||
// Delete shareholder
|
||||
self.engine.register_result_fn("delete_shareholder",
|
||||
|id: i64| -> Result<(), Box<EvalAltResult>> {
|
||||
if id <= 0 {
|
||||
return Err(format!("Invalid shareholder ID: {}", id).into());
|
||||
}
|
||||
|
||||
// Implementation would delete from database
|
||||
Err(format!("Failed to delete shareholder with ID {}", id).into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper function to safely convert a map to a strongly-typed object
|
||||
pub fn map_to_object<T>(&self, map: &Map, converter: fn(&Map) -> Result<T, String>) -> Result<T, String> {
|
||||
converter(map)
|
||||
}
|
||||
|
||||
/// Helper function to handle Dynamic types safely
|
||||
pub fn get_dynamic_value(&self, dynamic: &Dynamic, key: &str) -> Result<Dynamic, String> {
|
||||
if let Some(map) = dynamic.try_cast::<Map>() {
|
||||
match map.get(key) {
|
||||
Some(value) => Ok(value.clone()),
|
||||
None => Err(format!("Key '{}' not found in map", key))
|
||||
}
|
||||
} else {
|
||||
Err("Dynamic value is not a map".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to extract a value of a specific type from a Dynamic value
|
||||
pub fn extract_value<T: Clone + 'static>(&self, dynamic: &Dynamic, key: &str) -> Result<T, String>
|
||||
where Dynamic: TryInto<T> {
|
||||
let value = self.get_dynamic_value(dynamic, key)?;
|
||||
match value.clone().try_into() {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(format!("Value for key '{}' has wrong type", key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Rhai Engine module for scripting support
|
||||
//!
|
||||
//! This module provides integration with the Rhai scripting language.
|
||||
|
||||
// Re-export the engine module
|
||||
pub mod engine;
|
||||
pub use engine::*;
|
||||
Reference in New Issue
Block a user