This commit is contained in:
2025-04-22 12:53:17 +04:00
parent cad285fd59
commit 1708e6dd06
116 changed files with 1919 additions and 81 deletions

View File

@@ -0,0 +1,530 @@
# 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);
```

View File

@@ -0,0 +1,293 @@
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
}
}

View File

@@ -0,0 +1,114 @@
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
}
}

View File

@@ -0,0 +1,166 @@
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
}
}

View File

@@ -0,0 +1,178 @@
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
};
}

View File

@@ -0,0 +1,577 @@
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
}
}

View File

@@ -0,0 +1,30 @@
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};

View File

@@ -0,0 +1,28 @@
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};

View File

@@ -0,0 +1,425 @@
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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
[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"

View File

@@ -0,0 +1,168 @@
// 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!");

View File

@@ -0,0 +1,41 @@
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()))
}
}

View File

@@ -0,0 +1,88 @@
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
});
}

View File

@@ -0,0 +1,132 @@
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
}
}

View File

@@ -0,0 +1,11 @@
// 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

View File

@@ -0,0 +1,640 @@
//! 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
)))
}
}

View File

@@ -0,0 +1,579 @@
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
}
}

View File

@@ -0,0 +1,478 @@
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
}
}

View File

@@ -0,0 +1,35 @@
# 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.

View File

@@ -0,0 +1,43 @@
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"
}
}

View File

@@ -0,0 +1,9 @@
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};

View File

@@ -0,0 +1,83 @@
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"
}
}

View File

@@ -0,0 +1,13 @@
pub mod circle;
pub mod member;
pub mod name;
pub mod wallet;
// Re-export all model types for convenience
pub use circle::Circle;
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};

View File

@@ -0,0 +1,73 @@
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"
}
}

View File

@@ -0,0 +1,85 @@
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"
}
}

View File

@@ -0,0 +1,355 @@
# 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.

View File

@@ -0,0 +1,149 @@
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"
}
}

View File

@@ -0,0 +1,185 @@
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>> { ... }
}

View File

@@ -0,0 +1,188 @@
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"
}
}

View File

@@ -0,0 +1,20 @@
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};

View File

@@ -0,0 +1,195 @@
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"
}
}

View File

@@ -0,0 +1,77 @@
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"
}
}

View File

@@ -0,0 +1,56 @@
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"
}
}

View File

@@ -0,0 +1,150 @@
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"
}
}

View File

@@ -0,0 +1,21 @@
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)
-

View File

@@ -0,0 +1,96 @@
# 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

View File

@@ -0,0 +1,56 @@
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);

View File

@@ -0,0 +1,90 @@
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);

View File

@@ -0,0 +1,131 @@
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);

View File

@@ -0,0 +1,12 @@
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};

View File

@@ -0,0 +1,129 @@
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);

View File

@@ -0,0 +1,122 @@
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);

View File

@@ -0,0 +1,12 @@
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};

View File

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

View File

@@ -0,0 +1,131 @@
# Business Models Python Port
This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration.
## Overview
This project includes:
1. Python port of Rust business models using SQLModel
2. FastAPI server with OpenAPI/Swagger documentation
3. CRUD operations for all models
4. Convenience endpoints for common operations
The models ported from Rust to Python include:
- **Currency**: Represents a monetary value with amount and currency code
- **Customer**: Represents a customer who can purchase products or services
- **Product**: Represents a product or service offered
- **ProductComponent**: Represents a component of a product
- **SaleItem**: Represents an item in a sale
- **Sale**: Represents a sale of products or services
## Structure
- `models.py`: Contains the SQLModel definitions for all business models
- `example.py`: Demonstrates how to use the models with a sample application
- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example
- `api.py`: FastAPI server providing CRUD operations for all models
- `server.sh`: Bash script to start the FastAPI server
## Requirements
- Python 3.7+
- [uv](https://github.com/astral-sh/uv) for dependency management
## Installation
The project uses `uv` for dependency management. To install dependencies and run the example:
```bash
./install_and_run.sh
```
## API Server
The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints.
### Starting the Server
To start the API server:
```bash
./server.sh
```
This script will:
1. Create a virtual environment if it doesn't exist
2. Install the required dependencies using `uv`
3. Start the FastAPI server with hot reloading enabled
### API Documentation
Once the server is running, you can access the OpenAPI documentation at:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
### Available Endpoints
The API provides the following endpoints:
#### Currencies
- `GET /currencies/`: List all currencies
- `POST /currencies/`: Create a new currency
- `GET /currencies/{currency_id}`: Get a specific currency
- `PUT /currencies/{currency_id}`: Update a currency
- `DELETE /currencies/{currency_id}`: Delete a currency
#### Customers
- `GET /customers/`: List all customers
- `POST /customers/`: Create a new customer
- `GET /customers/{customer_id}`: Get a specific customer
- `PUT /customers/{customer_id}`: Update a customer
- `DELETE /customers/{customer_id}`: Delete a customer
- `GET /customers/{customer_id}/sales/`: Get all sales for a customer
#### Products
- `GET /products/`: List all products
- `POST /products/`: Create a new product
- `GET /products/{product_id}`: Get a specific product
- `PUT /products/{product_id}`: Update a product
- `DELETE /products/{product_id}`: Delete a product
- `GET /products/available/`: Get all available products
- `POST /products/{product_id}/components/`: Add a component to a product
- `GET /products/{product_id}/components/`: Get all components for a product
#### Sales
- `GET /sales/`: List all sales
- `POST /sales/`: Create a new sale
- `GET /sales/{sale_id}`: Get a specific sale
- `PUT /sales/{sale_id}`: Update a sale
- `DELETE /sales/{sale_id}`: Delete a sale
- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale
- `POST /sales/{sale_id}/items/`: Add an item to a sale
- `GET /sales/{sale_id}/items/`: Get all items for a sale
## Dependencies
- SQLModel: For database models and ORM functionality
- Pydantic: For data validation (used by SQLModel)
- FastAPI: For creating the API server
- Uvicorn: ASGI server for running FastAPI applications
## Example Usage
The `example.py` script demonstrates:
1. Creating an SQLite database
2. Defining and creating tables for the models
3. Creating sample data (customers, products, sales)
4. Performing operations on the data
5. Querying and displaying the data
To run the example manually (after activating the virtual environment):
```bash
# From the py directory
python example.py
# Or from the parent directory
cd py && python example.py

View File

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

455
herodb_old/src/models/py/api.py Executable file
View File

@@ -0,0 +1,455 @@
#!/usr/bin/env python3
"""
FastAPI server providing CRUD operations for business models.
"""
import os
from datetime import datetime
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import Session, SQLModel, create_engine, select
from models import (
Currency,
Customer,
Product,
ProductComponent,
ProductStatus,
ProductType,
Sale,
SaleItem,
SaleStatus,
)
# Create database
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
engine = create_engine(DATABASE_URL, echo=False)
# Create tables
SQLModel.metadata.create_all(engine)
# Create FastAPI app
app = FastAPI(
title="Business API",
description="API for business models",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Dependency to get database session
def get_session():
with Session(engine) as session:
yield session
# Root endpoint
@app.get("/")
async def root():
return {"message": "Welcome to the Business API"}
# Currency endpoints
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
def create_currency(currency: Currency, session: Session = Depends(get_session)):
"""Create a new currency"""
session.add(currency)
session.commit()
session.refresh(currency)
return currency
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
def read_currencies(
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
):
"""Get all currencies"""
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
return currencies
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
def read_currency(currency_id: int, session: Session = Depends(get_session)):
"""Get a currency by ID"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
return currency
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
def update_currency(
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
):
"""Update a currency"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
# Update currency attributes
currency_data_dict = currency_data.dict(exclude_unset=True)
for key, value in currency_data_dict.items():
setattr(currency, key, value)
session.add(currency)
session.commit()
session.refresh(currency)
return currency
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
"""Delete a currency"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
session.delete(currency)
session.commit()
return {"message": "Currency deleted successfully"}
# Customer endpoints
@app.post("/customers/", response_model=Customer, tags=["Customers"])
def create_customer(customer: Customer, session: Session = Depends(get_session)):
"""Create a new customer"""
session.add(customer)
session.commit()
session.refresh(customer)
return customer
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
def read_customers(
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
):
"""Get all customers"""
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
return customers
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
def read_customer(customer_id: int, session: Session = Depends(get_session)):
"""Get a customer by ID"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
return customer
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
def update_customer(
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
):
"""Update a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Update customer attributes
customer_data_dict = customer_data.dict(exclude_unset=True)
for key, value in customer_data_dict.items():
setattr(customer, key, value)
session.add(customer)
session.commit()
session.refresh(customer)
return customer
@app.delete("/customers/{customer_id}", tags=["Customers"])
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
"""Delete a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
session.delete(customer)
session.commit()
return {"message": "Customer deleted successfully"}
# Product endpoints
@app.post("/products/", response_model=Product, tags=["Products"])
def create_product(product: Product, session: Session = Depends(get_session)):
"""Create a new product"""
session.add(product)
session.commit()
session.refresh(product)
return product
@app.get("/products/", response_model=List[Product], tags=["Products"])
def read_products(
skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
status: Optional[ProductStatus] = None,
istemplate: Optional[bool] = None,
session: Session = Depends(get_session)
):
"""Get all products with optional filtering"""
query = select(Product)
if category:
query = query.where(Product.category == category)
if status:
query = query.where(Product.status == status)
if istemplate is not None:
query = query.where(Product.istemplate == istemplate)
products = session.exec(query.offset(skip).limit(limit)).all()
return products
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
def read_product(product_id: int, session: Session = Depends(get_session)):
"""Get a product by ID"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
def update_product(
product_id: int, product_data: Product, session: Session = Depends(get_session)
):
"""Update a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Update product attributes
product_data_dict = product_data.dict(exclude_unset=True)
for key, value in product_data_dict.items():
setattr(product, key, value)
session.add(product)
session.commit()
session.refresh(product)
return product
@app.delete("/products/{product_id}", tags=["Products"])
def delete_product(product_id: int, session: Session = Depends(get_session)):
"""Delete a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
session.delete(product)
session.commit()
return {"message": "Product deleted successfully"}
# Product Component endpoints
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
def create_product_component(
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
):
"""Add a component to a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
component.product_id = product_id
session.add(component)
session.commit()
session.refresh(component)
return component
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
def read_product_components(
product_id: int, session: Session = Depends(get_session)
):
"""Get all components for a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product.components
# Sale endpoints
@app.post("/sales/", response_model=Sale, tags=["Sales"])
def create_sale(sale: Sale, session: Session = Depends(get_session)):
"""Create a new sale"""
# Ensure customer exists if customer_id is provided
if sale.customer_id:
customer = session.get(Customer, sale.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
def read_sales(
skip: int = 0,
limit: int = 100,
status: Optional[SaleStatus] = None,
customer_id: Optional[int] = None,
session: Session = Depends(get_session)
):
"""Get all sales with optional filtering"""
query = select(Sale)
if status:
query = query.where(Sale.status == status)
if customer_id:
query = query.where(Sale.customer_id == customer_id)
sales = session.exec(query.offset(skip).limit(limit)).all()
return sales
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
def read_sale(sale_id: int, session: Session = Depends(get_session)):
"""Get a sale by ID"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
return sale
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
def update_sale(
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
):
"""Update a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
# Update sale attributes
sale_data_dict = sale_data.dict(exclude_unset=True)
for key, value in sale_data_dict.items():
setattr(sale, key, value)
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.delete("/sales/{sale_id}", tags=["Sales"])
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
"""Delete a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
session.delete(sale)
session.commit()
return {"message": "Sale deleted successfully"}
# Sale Item endpoints
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
def create_sale_item(
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
):
"""Add an item to a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
item.sale_id = sale_id
session.add(item)
session.commit()
session.refresh(item)
# Update the sale's total amount
sale.add_item(item)
session.add(sale)
session.commit()
return item
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
def read_sale_items(
sale_id: int, session: Session = Depends(get_session)
):
"""Get all items for a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
return sale.items
# Convenience endpoints
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
def update_sale_status(
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
):
"""Update the status of a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
sale.update_status(status)
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
def get_available_products(
istemplate: Optional[bool] = False,
session: Session = Depends(get_session)
):
"""Get all available products"""
query = select(Product).where(
Product.status == ProductStatus.AVAILABLE,
Product.purchase_till > datetime.utcnow(),
Product.istemplate == istemplate
)
products = session.exec(query).all()
return products
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
def get_customer_sales(
customer_id: int,
status: Optional[SaleStatus] = None,
session: Session = Depends(get_session)
):
"""Get all sales for a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
query = select(Sale).where(Sale.customer_id == customer_id)
if status:
query = query.where(Sale.status == status)
sales = session.exec(query).all()
return sales
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

Binary file not shown.

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
Example script demonstrating the use of the business models.
"""
import datetime
from typing import List
from sqlmodel import Session, SQLModel, create_engine, select
from models import (
Currency,
Customer,
Product,
ProductComponent,
ProductStatus,
ProductType,
Sale,
SaleItem,
SaleStatus,
)
def create_tables(engine):
"""Create all tables in the database"""
SQLModel.metadata.create_all(engine)
def create_sample_data(session: Session) -> None:
"""Create sample data for demonstration"""
# Create currencies
usd = Currency(currency_code="USD", amount=0.0)
eur = Currency(currency_code="EUR", amount=0.0)
session.add(usd)
session.add(eur)
session.commit()
# Create a customer
customer = Customer.new(
name="Acme Corporation",
description="A fictional company",
pubkey="acme123456",
contact_sids=["circle1_contact123", "circle2_contact456"]
)
session.add(customer)
session.commit()
# Create product components
cpu_component = ProductComponent.new(
name="CPU",
description="Central Processing Unit",
quantity=1,
)
ram_component = ProductComponent.new(
name="RAM",
description="Random Access Memory",
quantity=2,
)
session.add(cpu_component)
session.add(ram_component)
session.commit()
# Create products
laptop_price = Currency(currency_code="USD", amount=1200.0)
session.add(laptop_price)
session.commit()
laptop = Product.new(
name="Laptop",
description="High-performance laptop",
price=laptop_price,
type_=ProductType.PRODUCT,
category="Electronics",
status=ProductStatus.AVAILABLE,
max_amount=100,
validity_days=365,
istemplate=False,
)
laptop.add_component(cpu_component)
laptop.add_component(ram_component)
session.add(laptop)
session.commit()
support_price = Currency(currency_code="USD", amount=50.0)
session.add(support_price)
session.commit()
support = Product.new(
name="Technical Support",
description="24/7 technical support",
price=support_price,
type_=ProductType.SERVICE,
category="Support",
status=ProductStatus.AVAILABLE,
max_amount=1000,
validity_days=30,
istemplate=True, # This is a template product
)
session.add(support)
session.commit()
# Create a sale
sale = Sale.new(
customer=customer,
currency_code="USD",
)
session.add(sale)
session.commit()
# Create sale items
laptop_unit_price = Currency(currency_code="USD", amount=1200.0)
session.add(laptop_unit_price)
session.commit()
laptop_item = SaleItem.new(
product=laptop,
quantity=1,
unit_price=laptop_unit_price,
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=365),
)
sale.add_item(laptop_item)
support_unit_price = Currency(currency_code="USD", amount=50.0)
session.add(support_unit_price)
session.commit()
support_item = SaleItem.new(
product=support,
quantity=2,
unit_price=support_unit_price,
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=30),
)
sale.add_item(support_item)
# Complete the sale
sale.update_status(SaleStatus.COMPLETED)
session.commit()
def query_data(session: Session) -> None:
"""Query and display data from the database"""
print("\n=== Customers ===")
customers = session.exec(select(Customer)).all()
for customer in customers:
print(f"Customer: {customer.name} ({customer.pubkey})")
print(f" Description: {customer.description}")
print(f" Contact SIDs: {', '.join(customer.contact_sids)}")
print(f" Created at: {customer.created_at}")
print("\n=== Products ===")
products = session.exec(select(Product)).all()
for product in products:
print(f"Product: {product.name} ({product.type_.value})")
print(f" Description: {product.description}")
print(f" Price: {product.price.amount} {product.price.currency_code}")
print(f" Status: {product.status.value}")
print(f" Is Template: {product.istemplate}")
print(f" Components:")
for component in product.components:
print(f" - {component.name}: {component.quantity}")
print("\n=== Sales ===")
sales = session.exec(select(Sale)).all()
for sale in sales:
print(f"Sale to: {sale.customer.name}")
print(f" Status: {sale.status.value}")
print(f" Total: {sale.total_amount.amount} {sale.total_amount.currency_code}")
print(f" Items:")
for item in sale.items:
print(f" - {item.name}: {item.quantity} x {item.unit_price.amount} = {item.subtotal.amount} {item.subtotal.currency_code}")
def main():
"""Main function"""
print("Creating in-memory SQLite database...")
engine = create_engine("sqlite:///business.db", echo=False)
print("Creating tables...")
create_tables(engine)
print("Creating sample data...")
with Session(engine) as session:
create_sample_data(session)
print("Querying data...")
with Session(engine) as session:
query_data(session)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Script to install dependencies using uv and run the example script
set -e # Exit on error
# Change to the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
echo "Changed to directory: $SCRIPT_DIR"
# Define variables
VENV_DIR=".venv"
REQUIREMENTS="sqlmodel pydantic"
# Check if uv is installed
if ! command -v uv &> /dev/null; then
echo "Error: uv is not installed."
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
exit 1
fi
# Create virtual environment if it doesn't exist
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
uv venv "$VENV_DIR"
fi
# Activate virtual environment
echo "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
# Install dependencies
echo "Installing dependencies using uv..."
uv pip install $REQUIREMENTS
# Make example.py executable
chmod +x example.py
# Remove existing database file if it exists
if [ -f "business.db" ]; then
echo "Removing existing database file..."
rm business.db
fi
# Run the example script
echo "Running example script..."
python example.py
echo "Done!"

View File

@@ -0,0 +1,297 @@
"""
Python port of the business models from Rust using SQLModel.
"""
from datetime import datetime, timedelta
from enum import Enum
import json
from typing import List, Optional
from sqlmodel import Field, Relationship, SQLModel
class SaleStatus(str, Enum):
"""SaleStatus represents the status of a sale"""
PENDING = "pending"
COMPLETED = "completed"
CANCELLED = "cancelled"
class ProductType(str, Enum):
"""ProductType represents the type of a product"""
PRODUCT = "product"
SERVICE = "service"
class ProductStatus(str, Enum):
"""ProductStatus represents the status of a product"""
AVAILABLE = "available"
UNAVAILABLE = "unavailable"
class Currency(SQLModel, table=True):
"""Currency represents a monetary value with amount and currency code"""
id: Optional[int] = Field(default=None, primary_key=True)
amount: float
currency_code: str
@classmethod
def new(cls, amount: float, currency_code: str) -> "Currency":
"""Create a new currency with amount and code"""
return cls(amount=amount, currency_code=currency_code)
class Customer(SQLModel, table=True):
"""Customer represents a customer who can purchase products or services"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
description: str
pubkey: str
contact_sids_json: str = Field(default="[]")
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
sales: List["Sale"] = Relationship(back_populates="customer")
@property
def contact_sids(self) -> List[str]:
"""Get the contact SIDs as a list"""
return json.loads(self.contact_sids_json)
@contact_sids.setter
def contact_sids(self, value: List[str]) -> None:
"""Set the contact SIDs from a list"""
self.contact_sids_json = json.dumps(value)
@classmethod
def new(cls, name: str, description: str, pubkey: str, contact_sids: List[str] = None) -> "Customer":
"""Create a new customer with default timestamps"""
customer = cls(
name=name,
description=description,
pubkey=pubkey,
)
if contact_sids:
customer.contact_sids = contact_sids
return customer
def add_contact(self, contact_id: int) -> None:
"""Add a contact ID to the customer"""
# In a real implementation, this would add a relationship to a Contact model
# For simplicity, we're not implementing the Contact model in this example
self.updated_at = datetime.utcnow()
def add_contact_sid(self, circle_id: str, object_id: str) -> None:
"""Add a smart ID (sid) to the customer's contact_sids list"""
sid = f"{circle_id}_{object_id}"
sids = self.contact_sids
if sid not in sids:
sids.append(sid)
self.contact_sids = sids
self.updated_at = datetime.utcnow()
class ProductComponent(SQLModel, table=True):
"""ProductComponent represents a component of a product"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
description: str
quantity: int
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
product: Optional["Product"] = Relationship(back_populates="components")
@classmethod
def new(cls, name: str, description: str, quantity: int) -> "ProductComponent":
"""Create a new product component with default timestamps"""
return cls(
name=name,
description=description,
quantity=quantity,
)
class Product(SQLModel, table=True):
"""Product represents a product or service offered"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
description: str
type_: ProductType = Field(sa_column_kwargs={"name": "type"})
category: str
status: ProductStatus
max_amount: int
purchase_till: datetime
active_till: datetime
istemplate: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Price relationship
price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
price: Optional[Currency] = Relationship()
# Relationships
components: List[ProductComponent] = Relationship(back_populates="product")
sale_items: List["SaleItem"] = Relationship(back_populates="product")
@classmethod
def new(
cls,
name: str,
description: str,
price: Currency,
type_: ProductType,
category: str,
status: ProductStatus,
max_amount: int,
validity_days: int,
istemplate: bool = False,
) -> "Product":
"""Create a new product with default timestamps"""
now = datetime.utcnow()
return cls(
name=name,
description=description,
price=price,
type_=type_,
category=category,
status=status,
max_amount=max_amount,
purchase_till=now + timedelta(days=365),
active_till=now + timedelta(days=validity_days),
istemplate=istemplate,
)
def add_component(self, component: ProductComponent) -> None:
"""Add a component to this product"""
component.product = self
self.components.append(component)
self.updated_at = datetime.utcnow()
def set_purchase_period(self, purchase_till: datetime) -> None:
"""Update the purchase availability timeframe"""
self.purchase_till = purchase_till
self.updated_at = datetime.utcnow()
def set_active_period(self, active_till: datetime) -> None:
"""Update the active timeframe"""
self.active_till = active_till
self.updated_at = datetime.utcnow()
def is_purchasable(self) -> bool:
"""Check if the product is available for purchase"""
return self.status == ProductStatus.AVAILABLE and datetime.utcnow() <= self.purchase_till
def is_active(self) -> bool:
"""Check if the product is still active (for services)"""
return datetime.utcnow() <= self.active_till
class SaleItem(SQLModel, table=True):
"""SaleItem represents an item in a sale"""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
quantity: int
active_till: datetime
# Relationships
sale_id: Optional[int] = Field(default=None, foreign_key="sale.id")
sale: Optional["Sale"] = Relationship(back_populates="items")
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
product: Optional[Product] = Relationship(back_populates="sale_items")
unit_price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
unit_price: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.unit_price_id]"})
subtotal_id: Optional[int] = Field(default=None, foreign_key="currency.id")
subtotal: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.subtotal_id]"})
@classmethod
def new(
cls,
product: Product,
quantity: int,
unit_price: Currency,
active_till: datetime,
) -> "SaleItem":
"""Create a new sale item"""
# Calculate subtotal
amount = unit_price.amount * quantity
subtotal = Currency(
amount=amount,
currency_code=unit_price.currency_code,
)
return cls(
name=product.name,
product=product,
quantity=quantity,
unit_price=unit_price,
subtotal=subtotal,
active_till=active_till,
)
class Sale(SQLModel, table=True):
"""Sale represents a sale of products or services"""
id: Optional[int] = Field(default=None, primary_key=True)
status: SaleStatus
sale_date: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
# Relationships
customer_id: Optional[int] = Field(default=None, foreign_key="customer.id")
customer: Optional[Customer] = Relationship(back_populates="sales")
total_amount_id: Optional[int] = Field(default=None, foreign_key="currency.id")
total_amount: Optional[Currency] = Relationship()
items: List[SaleItem] = Relationship(back_populates="sale")
@classmethod
def new(
cls,
customer: Customer,
currency_code: str,
status: SaleStatus = SaleStatus.PENDING,
) -> "Sale":
"""Create a new sale with default timestamps"""
total_amount = Currency(amount=0.0, currency_code=currency_code)
return cls(
customer=customer,
total_amount=total_amount,
status=status,
)
def add_item(self, item: SaleItem) -> None:
"""Add an item to the sale and update the total amount"""
item.sale = self
# Update the total amount
if not self.items:
# First item, initialize the total amount with the same currency
self.total_amount = Currency(
amount=item.subtotal.amount,
currency_code=item.subtotal.currency_code,
)
else:
# Add to the existing total
# (Assumes all items have the same currency)
self.total_amount.amount += item.subtotal.amount
# Add the item to the list
self.items.append(item)
# Update the sale timestamp
self.updated_at = datetime.utcnow()
def update_status(self, status: SaleStatus) -> None:
"""Update the status of the sale"""
self.status = status
self.updated_at = datetime.utcnow()

View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Script to start the FastAPI server
set -e # Exit on error
# Change to the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
echo "Changed to directory: $SCRIPT_DIR"
# Define variables
VENV_DIR=".venv"
REQUIREMENTS="sqlmodel pydantic fastapi uvicorn"
# Check if uv is installed
if ! command -v uv &> /dev/null; then
echo "Error: uv is not installed."
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
exit 1
fi
# Create virtual environment if it doesn't exist
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
uv venv "$VENV_DIR"
fi
# Activate virtual environment
echo "Activating virtual environment..."
source "$VENV_DIR/bin/activate"
# Install dependencies
echo "Installing dependencies using uv..."
uv pip install $REQUIREMENTS
# Make api.py executable
chmod +x api.py
# Start the FastAPI server
echo "Starting FastAPI server..."
echo "API documentation available at: http://localhost:8000/docs"
uvicorn api:app --host 0.0.0.0 --port 8000 --reload