...
This commit is contained in:
530
herodb_old/src/models/biz/README.md
Normal file
530
herodb_old/src/models/biz/README.md
Normal 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);
|
||||
```
|
293
herodb_old/src/models/biz/contract.rs
Normal file
293
herodb_old/src/models/biz/contract.rs
Normal 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
|
||||
}
|
||||
}
|
114
herodb_old/src/models/biz/currency.rs
Normal file
114
herodb_old/src/models/biz/currency.rs
Normal 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
|
||||
}
|
||||
}
|
166
herodb_old/src/models/biz/customer.rs
Normal file
166
herodb_old/src/models/biz/customer.rs
Normal 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
|
||||
}
|
||||
}
|
178
herodb_old/src/models/biz/exchange_rate.rs
Normal file
178
herodb_old/src/models/biz/exchange_rate.rs
Normal 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
|
||||
};
|
||||
}
|
577
herodb_old/src/models/biz/invoice.rs
Normal file
577
herodb_old/src/models/biz/invoice.rs
Normal 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
|
||||
}
|
||||
}
|
30
herodb_old/src/models/biz/lib.rs
Normal file
30
herodb_old/src/models/biz/lib.rs
Normal 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};
|
28
herodb_old/src/models/biz/mod.rs
Normal file
28
herodb_old/src/models/biz/mod.rs
Normal 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};
|
425
herodb_old/src/models/biz/product.rs
Normal file
425
herodb_old/src/models/biz/product.rs
Normal 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
2164
herodb_old/src/models/biz/rhai/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
herodb_old/src/models/biz/rhai/Cargo.toml
Normal file
19
herodb_old/src/models/biz/rhai/Cargo.toml
Normal 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"
|
168
herodb_old/src/models/biz/rhai/examples/example.rhai
Normal file
168
herodb_old/src/models/biz/rhai/examples/example.rhai
Normal 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!");
|
41
herodb_old/src/models/biz/rhai/examples/example.rs
Normal file
41
herodb_old/src/models/biz/rhai/examples/example.rs
Normal 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()))
|
||||
}
|
||||
}
|
88
herodb_old/src/models/biz/rhai/src/engine.rs
Normal file
88
herodb_old/src/models/biz/rhai/src/engine.rs
Normal 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
|
||||
});
|
||||
}
|
132
herodb_old/src/models/biz/rhai/src/generic_wrapper.rs
Normal file
132
herodb_old/src/models/biz/rhai/src/generic_wrapper.rs
Normal 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
|
||||
}
|
||||
}
|
11
herodb_old/src/models/biz/rhai/src/lib.rs
Normal file
11
herodb_old/src/models/biz/rhai/src/lib.rs
Normal 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
|
640
herodb_old/src/models/biz/rhai/src/wrapper.rs
Normal file
640
herodb_old/src/models/biz/rhai/src/wrapper.rs
Normal 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
|
||||
)))
|
||||
}
|
||||
}
|
579
herodb_old/src/models/biz/sale.rs
Normal file
579
herodb_old/src/models/biz/sale.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
478
herodb_old/src/models/biz/service.rs
Normal file
478
herodb_old/src/models/biz/service.rs
Normal 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
|
||||
}
|
||||
}
|
35
herodb_old/src/models/circle/README.md
Normal file
35
herodb_old/src/models/circle/README.md
Normal 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.
|
||||
|
43
herodb_old/src/models/circle/circle.rs
Normal file
43
herodb_old/src/models/circle/circle.rs
Normal 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"
|
||||
}
|
||||
}
|
9
herodb_old/src/models/circle/lib.rs
Normal file
9
herodb_old/src/models/circle/lib.rs
Normal 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};
|
83
herodb_old/src/models/circle/member.rs
Normal file
83
herodb_old/src/models/circle/member.rs
Normal 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"
|
||||
}
|
||||
}
|
13
herodb_old/src/models/circle/mod.rs
Normal file
13
herodb_old/src/models/circle/mod.rs
Normal 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};
|
73
herodb_old/src/models/circle/name.rs
Normal file
73
herodb_old/src/models/circle/name.rs
Normal 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"
|
||||
}
|
||||
}
|
85
herodb_old/src/models/circle/wallet.rs
Normal file
85
herodb_old/src/models/circle/wallet.rs
Normal 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"
|
||||
}
|
||||
}
|
355
herodb_old/src/models/gov/README.md
Normal file
355
herodb_old/src/models/gov/README.md
Normal 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.
|
149
herodb_old/src/models/gov/committee.rs
Normal file
149
herodb_old/src/models/gov/committee.rs
Normal 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"
|
||||
}
|
||||
}
|
185
herodb_old/src/models/gov/company.rs
Normal file
185
herodb_old/src/models/gov/company.rs
Normal 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>> { ... }
|
||||
}
|
188
herodb_old/src/models/gov/meeting.rs
Normal file
188
herodb_old/src/models/gov/meeting.rs
Normal 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"
|
||||
}
|
||||
}
|
20
herodb_old/src/models/gov/mod.rs
Normal file
20
herodb_old/src/models/gov/mod.rs
Normal 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};
|
195
herodb_old/src/models/gov/resolution.rs
Normal file
195
herodb_old/src/models/gov/resolution.rs
Normal 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"
|
||||
}
|
||||
}
|
77
herodb_old/src/models/gov/shareholder.rs
Normal file
77
herodb_old/src/models/gov/shareholder.rs
Normal 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"
|
||||
}
|
||||
}
|
56
herodb_old/src/models/gov/user.rs
Normal file
56
herodb_old/src/models/gov/user.rs
Normal 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"
|
||||
}
|
||||
}
|
150
herodb_old/src/models/gov/vote.rs
Normal file
150
herodb_old/src/models/gov/vote.rs
Normal 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"
|
||||
}
|
||||
}
|
21
herodb_old/src/models/instructions.md
Normal file
21
herodb_old/src/models/instructions.md
Normal 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)
|
||||
-
|
96
herodb_old/src/models/mcc/README.md
Normal file
96
herodb_old/src/models/mcc/README.md
Normal 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
|
56
herodb_old/src/models/mcc/calendar.rs
Normal file
56
herodb_old/src/models/mcc/calendar.rs
Normal 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);
|
90
herodb_old/src/models/mcc/contacts.rs
Normal file
90
herodb_old/src/models/mcc/contacts.rs
Normal 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);
|
131
herodb_old/src/models/mcc/event.rs
Normal file
131
herodb_old/src/models/mcc/event.rs
Normal 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);
|
12
herodb_old/src/models/mcc/lib.rs
Normal file
12
herodb_old/src/models/mcc/lib.rs
Normal 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};
|
129
herodb_old/src/models/mcc/mail.rs
Normal file
129
herodb_old/src/models/mcc/mail.rs
Normal 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);
|
122
herodb_old/src/models/mcc/message.rs
Normal file
122
herodb_old/src/models/mcc/message.rs
Normal 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);
|
12
herodb_old/src/models/mcc/mod.rs
Normal file
12
herodb_old/src/models/mcc/mod.rs
Normal 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};
|
4
herodb_old/src/models/mod.rs
Normal file
4
herodb_old/src/models/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod biz;
|
||||
pub mod mcc;
|
||||
pub mod circle;
|
||||
pub mod gov;
|
131
herodb_old/src/models/py/README.md
Normal file
131
herodb_old/src/models/py/README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Business Models Python Port
|
||||
|
||||
This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration.
|
||||
|
||||
## Overview
|
||||
|
||||
This project includes:
|
||||
|
||||
1. Python port of Rust business models using SQLModel
|
||||
2. FastAPI server with OpenAPI/Swagger documentation
|
||||
3. CRUD operations for all models
|
||||
4. Convenience endpoints for common operations
|
||||
|
||||
The models ported from Rust to Python include:
|
||||
|
||||
- **Currency**: Represents a monetary value with amount and currency code
|
||||
- **Customer**: Represents a customer who can purchase products or services
|
||||
- **Product**: Represents a product or service offered
|
||||
- **ProductComponent**: Represents a component of a product
|
||||
- **SaleItem**: Represents an item in a sale
|
||||
- **Sale**: Represents a sale of products or services
|
||||
|
||||
## Structure
|
||||
|
||||
- `models.py`: Contains the SQLModel definitions for all business models
|
||||
- `example.py`: Demonstrates how to use the models with a sample application
|
||||
- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example
|
||||
- `api.py`: FastAPI server providing CRUD operations for all models
|
||||
- `server.sh`: Bash script to start the FastAPI server
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- [uv](https://github.com/astral-sh/uv) for dependency management
|
||||
|
||||
## Installation
|
||||
|
||||
The project uses `uv` for dependency management. To install dependencies and run the example:
|
||||
|
||||
```bash
|
||||
./install_and_run.sh
|
||||
```
|
||||
|
||||
## API Server
|
||||
|
||||
The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints.
|
||||
|
||||
### Starting the Server
|
||||
|
||||
To start the API server:
|
||||
|
||||
```bash
|
||||
./server.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Create a virtual environment if it doesn't exist
|
||||
2. Install the required dependencies using `uv`
|
||||
3. Start the FastAPI server with hot reloading enabled
|
||||
|
||||
### API Documentation
|
||||
|
||||
Once the server is running, you can access the OpenAPI documentation at:
|
||||
|
||||
- Swagger UI: http://localhost:8000/docs
|
||||
- ReDoc: http://localhost:8000/redoc
|
||||
|
||||
### Available Endpoints
|
||||
|
||||
The API provides the following endpoints:
|
||||
|
||||
#### Currencies
|
||||
- `GET /currencies/`: List all currencies
|
||||
- `POST /currencies/`: Create a new currency
|
||||
- `GET /currencies/{currency_id}`: Get a specific currency
|
||||
- `PUT /currencies/{currency_id}`: Update a currency
|
||||
- `DELETE /currencies/{currency_id}`: Delete a currency
|
||||
|
||||
#### Customers
|
||||
- `GET /customers/`: List all customers
|
||||
- `POST /customers/`: Create a new customer
|
||||
- `GET /customers/{customer_id}`: Get a specific customer
|
||||
- `PUT /customers/{customer_id}`: Update a customer
|
||||
- `DELETE /customers/{customer_id}`: Delete a customer
|
||||
- `GET /customers/{customer_id}/sales/`: Get all sales for a customer
|
||||
|
||||
#### Products
|
||||
- `GET /products/`: List all products
|
||||
- `POST /products/`: Create a new product
|
||||
- `GET /products/{product_id}`: Get a specific product
|
||||
- `PUT /products/{product_id}`: Update a product
|
||||
- `DELETE /products/{product_id}`: Delete a product
|
||||
- `GET /products/available/`: Get all available products
|
||||
- `POST /products/{product_id}/components/`: Add a component to a product
|
||||
- `GET /products/{product_id}/components/`: Get all components for a product
|
||||
|
||||
#### Sales
|
||||
- `GET /sales/`: List all sales
|
||||
- `POST /sales/`: Create a new sale
|
||||
- `GET /sales/{sale_id}`: Get a specific sale
|
||||
- `PUT /sales/{sale_id}`: Update a sale
|
||||
- `DELETE /sales/{sale_id}`: Delete a sale
|
||||
- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale
|
||||
- `POST /sales/{sale_id}/items/`: Add an item to a sale
|
||||
- `GET /sales/{sale_id}/items/`: Get all items for a sale
|
||||
|
||||
## Dependencies
|
||||
|
||||
- SQLModel: For database models and ORM functionality
|
||||
- Pydantic: For data validation (used by SQLModel)
|
||||
- FastAPI: For creating the API server
|
||||
- Uvicorn: ASGI server for running FastAPI applications
|
||||
|
||||
## Example Usage
|
||||
|
||||
The `example.py` script demonstrates:
|
||||
|
||||
1. Creating an SQLite database
|
||||
2. Defining and creating tables for the models
|
||||
3. Creating sample data (customers, products, sales)
|
||||
4. Performing operations on the data
|
||||
5. Querying and displaying the data
|
||||
|
||||
To run the example manually (after activating the virtual environment):
|
||||
|
||||
```bash
|
||||
# From the py directory
|
||||
python example.py
|
||||
|
||||
# Or from the parent directory
|
||||
cd py && python example.py
|
3
herodb_old/src/models/py/__init__.py
Normal file
3
herodb_old/src/models/py/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Python port of the business models from Rust.
|
||||
"""
|
BIN
herodb_old/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
BIN
herodb_old/src/models/py/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
herodb_old/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
BIN
herodb_old/src/models/py/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
455
herodb_old/src/models/py/api.py
Executable file
455
herodb_old/src/models/py/api.py
Executable file
@@ -0,0 +1,455 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FastAPI server providing CRUD operations for business models.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import (
|
||||
Currency,
|
||||
Customer,
|
||||
Product,
|
||||
ProductComponent,
|
||||
ProductStatus,
|
||||
ProductType,
|
||||
Sale,
|
||||
SaleItem,
|
||||
SaleStatus,
|
||||
)
|
||||
|
||||
# Create database
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
|
||||
# Create tables
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# Create FastAPI app
|
||||
app = FastAPI(
|
||||
title="Business API",
|
||||
description="API for business models",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Dependency to get database session
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to the Business API"}
|
||||
|
||||
|
||||
# Currency endpoints
|
||||
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
|
||||
def create_currency(currency: Currency, session: Session = Depends(get_session)):
|
||||
"""Create a new currency"""
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
|
||||
def read_currencies(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all currencies"""
|
||||
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
|
||||
return currencies
|
||||
|
||||
|
||||
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def read_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a currency by ID"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
return currency
|
||||
|
||||
|
||||
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
|
||||
def update_currency(
|
||||
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
# Update currency attributes
|
||||
currency_data_dict = currency_data.dict(exclude_unset=True)
|
||||
for key, value in currency_data_dict.items():
|
||||
setattr(currency, key, value)
|
||||
|
||||
session.add(currency)
|
||||
session.commit()
|
||||
session.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
|
||||
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a currency"""
|
||||
currency = session.get(Currency, currency_id)
|
||||
if not currency:
|
||||
raise HTTPException(status_code=404, detail="Currency not found")
|
||||
|
||||
session.delete(currency)
|
||||
session.commit()
|
||||
return {"message": "Currency deleted successfully"}
|
||||
|
||||
|
||||
# Customer endpoints
|
||||
@app.post("/customers/", response_model=Customer, tags=["Customers"])
|
||||
def create_customer(customer: Customer, session: Session = Depends(get_session)):
|
||||
"""Create a new customer"""
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
|
||||
def read_customers(
|
||||
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all customers"""
|
||||
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
|
||||
return customers
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def read_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a customer by ID"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
|
||||
def update_customer(
|
||||
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Update customer attributes
|
||||
customer_data_dict = customer_data.dict(exclude_unset=True)
|
||||
for key, value in customer_data_dict.items():
|
||||
setattr(customer, key, value)
|
||||
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
session.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@app.delete("/customers/{customer_id}", tags=["Customers"])
|
||||
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.delete(customer)
|
||||
session.commit()
|
||||
return {"message": "Customer deleted successfully"}
|
||||
|
||||
|
||||
# Product endpoints
|
||||
@app.post("/products/", response_model=Product, tags=["Products"])
|
||||
def create_product(product: Product, session: Session = Depends(get_session)):
|
||||
"""Create a new product"""
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.get("/products/", response_model=List[Product], tags=["Products"])
|
||||
def read_products(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
category: Optional[str] = None,
|
||||
status: Optional[ProductStatus] = None,
|
||||
istemplate: Optional[bool] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all products with optional filtering"""
|
||||
query = select(Product)
|
||||
|
||||
if category:
|
||||
query = query.where(Product.category == category)
|
||||
|
||||
if status:
|
||||
query = query.where(Product.status == status)
|
||||
|
||||
if istemplate is not None:
|
||||
query = query.where(Product.istemplate == istemplate)
|
||||
|
||||
products = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def read_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a product by ID"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
return product
|
||||
|
||||
|
||||
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
|
||||
def update_product(
|
||||
product_id: int, product_data: Product, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# Update product attributes
|
||||
product_data_dict = product_data.dict(exclude_unset=True)
|
||||
for key, value in product_data_dict.items():
|
||||
setattr(product, key, value)
|
||||
|
||||
session.add(product)
|
||||
session.commit()
|
||||
session.refresh(product)
|
||||
return product
|
||||
|
||||
|
||||
@app.delete("/products/{product_id}", tags=["Products"])
|
||||
def delete_product(product_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
session.delete(product)
|
||||
session.commit()
|
||||
return {"message": "Product deleted successfully"}
|
||||
|
||||
|
||||
# Product Component endpoints
|
||||
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
|
||||
def create_product_component(
|
||||
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add a component to a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
component.product_id = product_id
|
||||
session.add(component)
|
||||
session.commit()
|
||||
session.refresh(component)
|
||||
return component
|
||||
|
||||
|
||||
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
|
||||
def read_product_components(
|
||||
product_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all components for a product"""
|
||||
product = session.get(Product, product_id)
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
return product.components
|
||||
|
||||
|
||||
# Sale endpoints
|
||||
@app.post("/sales/", response_model=Sale, tags=["Sales"])
|
||||
def create_sale(sale: Sale, session: Session = Depends(get_session)):
|
||||
"""Create a new sale"""
|
||||
# Ensure customer exists if customer_id is provided
|
||||
if sale.customer_id:
|
||||
customer = session.get(Customer, sale.customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
|
||||
def read_sales(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[SaleStatus] = None,
|
||||
customer_id: Optional[int] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales with optional filtering"""
|
||||
query = select(Sale)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
if customer_id:
|
||||
query = query.where(Sale.customer_id == customer_id)
|
||||
|
||||
sales = session.exec(query.offset(skip).limit(limit)).all()
|
||||
return sales
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def read_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Get a sale by ID"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
return sale
|
||||
|
||||
|
||||
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
|
||||
def update_sale(
|
||||
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
# Update sale attributes
|
||||
sale_data_dict = sale_data.dict(exclude_unset=True)
|
||||
for key, value in sale_data_dict.items():
|
||||
setattr(sale, key, value)
|
||||
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.delete("/sales/{sale_id}", tags=["Sales"])
|
||||
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
|
||||
"""Delete a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
session.delete(sale)
|
||||
session.commit()
|
||||
return {"message": "Sale deleted successfully"}
|
||||
|
||||
|
||||
# Sale Item endpoints
|
||||
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
|
||||
def create_sale_item(
|
||||
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Add an item to a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
item.sale_id = sale_id
|
||||
session.add(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
|
||||
# Update the sale's total amount
|
||||
sale.add_item(item)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
|
||||
return item
|
||||
|
||||
|
||||
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
|
||||
def read_sale_items(
|
||||
sale_id: int, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all items for a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
return sale.items
|
||||
|
||||
|
||||
# Convenience endpoints
|
||||
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
|
||||
def update_sale_status(
|
||||
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update the status of a sale"""
|
||||
sale = session.get(Sale, sale_id)
|
||||
if not sale:
|
||||
raise HTTPException(status_code=404, detail="Sale not found")
|
||||
|
||||
sale.update_status(status)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
session.refresh(sale)
|
||||
return sale
|
||||
|
||||
|
||||
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
|
||||
def get_available_products(
|
||||
istemplate: Optional[bool] = False,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all available products"""
|
||||
query = select(Product).where(
|
||||
Product.status == ProductStatus.AVAILABLE,
|
||||
Product.purchase_till > datetime.utcnow(),
|
||||
Product.istemplate == istemplate
|
||||
)
|
||||
products = session.exec(query).all()
|
||||
return products
|
||||
|
||||
|
||||
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
|
||||
def get_customer_sales(
|
||||
customer_id: int,
|
||||
status: Optional[SaleStatus] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all sales for a customer"""
|
||||
customer = session.get(Customer, customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
query = select(Sale).where(Sale.customer_id == customer_id)
|
||||
|
||||
if status:
|
||||
query = query.where(Sale.status == status)
|
||||
|
||||
sales = session.exec(query).all()
|
||||
return sales
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
BIN
herodb_old/src/models/py/business.db
Normal file
BIN
herodb_old/src/models/py/business.db
Normal file
Binary file not shown.
190
herodb_old/src/models/py/example.py
Executable file
190
herodb_old/src/models/py/example.py
Executable file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example script demonstrating the use of the business models.
|
||||
"""
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from models import (
|
||||
Currency,
|
||||
Customer,
|
||||
Product,
|
||||
ProductComponent,
|
||||
ProductStatus,
|
||||
ProductType,
|
||||
Sale,
|
||||
SaleItem,
|
||||
SaleStatus,
|
||||
)
|
||||
|
||||
|
||||
def create_tables(engine):
|
||||
"""Create all tables in the database"""
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
def create_sample_data(session: Session) -> None:
|
||||
"""Create sample data for demonstration"""
|
||||
# Create currencies
|
||||
usd = Currency(currency_code="USD", amount=0.0)
|
||||
eur = Currency(currency_code="EUR", amount=0.0)
|
||||
session.add(usd)
|
||||
session.add(eur)
|
||||
session.commit()
|
||||
|
||||
# Create a customer
|
||||
customer = Customer.new(
|
||||
name="Acme Corporation",
|
||||
description="A fictional company",
|
||||
pubkey="acme123456",
|
||||
contact_sids=["circle1_contact123", "circle2_contact456"]
|
||||
)
|
||||
session.add(customer)
|
||||
session.commit()
|
||||
|
||||
# Create product components
|
||||
cpu_component = ProductComponent.new(
|
||||
name="CPU",
|
||||
description="Central Processing Unit",
|
||||
quantity=1,
|
||||
)
|
||||
ram_component = ProductComponent.new(
|
||||
name="RAM",
|
||||
description="Random Access Memory",
|
||||
quantity=2,
|
||||
)
|
||||
session.add(cpu_component)
|
||||
session.add(ram_component)
|
||||
session.commit()
|
||||
|
||||
# Create products
|
||||
laptop_price = Currency(currency_code="USD", amount=1200.0)
|
||||
session.add(laptop_price)
|
||||
session.commit()
|
||||
|
||||
laptop = Product.new(
|
||||
name="Laptop",
|
||||
description="High-performance laptop",
|
||||
price=laptop_price,
|
||||
type_=ProductType.PRODUCT,
|
||||
category="Electronics",
|
||||
status=ProductStatus.AVAILABLE,
|
||||
max_amount=100,
|
||||
validity_days=365,
|
||||
istemplate=False,
|
||||
)
|
||||
laptop.add_component(cpu_component)
|
||||
laptop.add_component(ram_component)
|
||||
session.add(laptop)
|
||||
session.commit()
|
||||
|
||||
support_price = Currency(currency_code="USD", amount=50.0)
|
||||
session.add(support_price)
|
||||
session.commit()
|
||||
|
||||
support = Product.new(
|
||||
name="Technical Support",
|
||||
description="24/7 technical support",
|
||||
price=support_price,
|
||||
type_=ProductType.SERVICE,
|
||||
category="Support",
|
||||
status=ProductStatus.AVAILABLE,
|
||||
max_amount=1000,
|
||||
validity_days=30,
|
||||
istemplate=True, # This is a template product
|
||||
)
|
||||
session.add(support)
|
||||
session.commit()
|
||||
|
||||
# Create a sale
|
||||
sale = Sale.new(
|
||||
customer=customer,
|
||||
currency_code="USD",
|
||||
)
|
||||
session.add(sale)
|
||||
session.commit()
|
||||
|
||||
# Create sale items
|
||||
laptop_unit_price = Currency(currency_code="USD", amount=1200.0)
|
||||
session.add(laptop_unit_price)
|
||||
session.commit()
|
||||
|
||||
laptop_item = SaleItem.new(
|
||||
product=laptop,
|
||||
quantity=1,
|
||||
unit_price=laptop_unit_price,
|
||||
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=365),
|
||||
)
|
||||
sale.add_item(laptop_item)
|
||||
|
||||
support_unit_price = Currency(currency_code="USD", amount=50.0)
|
||||
session.add(support_unit_price)
|
||||
session.commit()
|
||||
|
||||
support_item = SaleItem.new(
|
||||
product=support,
|
||||
quantity=2,
|
||||
unit_price=support_unit_price,
|
||||
active_till=datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||
)
|
||||
sale.add_item(support_item)
|
||||
|
||||
# Complete the sale
|
||||
sale.update_status(SaleStatus.COMPLETED)
|
||||
session.commit()
|
||||
|
||||
|
||||
def query_data(session: Session) -> None:
|
||||
"""Query and display data from the database"""
|
||||
print("\n=== Customers ===")
|
||||
customers = session.exec(select(Customer)).all()
|
||||
for customer in customers:
|
||||
print(f"Customer: {customer.name} ({customer.pubkey})")
|
||||
print(f" Description: {customer.description}")
|
||||
print(f" Contact SIDs: {', '.join(customer.contact_sids)}")
|
||||
print(f" Created at: {customer.created_at}")
|
||||
|
||||
print("\n=== Products ===")
|
||||
products = session.exec(select(Product)).all()
|
||||
for product in products:
|
||||
print(f"Product: {product.name} ({product.type_.value})")
|
||||
print(f" Description: {product.description}")
|
||||
print(f" Price: {product.price.amount} {product.price.currency_code}")
|
||||
print(f" Status: {product.status.value}")
|
||||
print(f" Is Template: {product.istemplate}")
|
||||
print(f" Components:")
|
||||
for component in product.components:
|
||||
print(f" - {component.name}: {component.quantity}")
|
||||
|
||||
print("\n=== Sales ===")
|
||||
sales = session.exec(select(Sale)).all()
|
||||
for sale in sales:
|
||||
print(f"Sale to: {sale.customer.name}")
|
||||
print(f" Status: {sale.status.value}")
|
||||
print(f" Total: {sale.total_amount.amount} {sale.total_amount.currency_code}")
|
||||
print(f" Items:")
|
||||
for item in sale.items:
|
||||
print(f" - {item.name}: {item.quantity} x {item.unit_price.amount} = {item.subtotal.amount} {item.subtotal.currency_code}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("Creating in-memory SQLite database...")
|
||||
engine = create_engine("sqlite:///business.db", echo=False)
|
||||
|
||||
print("Creating tables...")
|
||||
create_tables(engine)
|
||||
|
||||
print("Creating sample data...")
|
||||
with Session(engine) as session:
|
||||
create_sample_data(session)
|
||||
|
||||
print("Querying data...")
|
||||
with Session(engine) as session:
|
||||
query_data(session)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
49
herodb_old/src/models/py/install_and_run.sh
Executable file
49
herodb_old/src/models/py/install_and_run.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# Script to install dependencies using uv and run the example script
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Change to the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Changed to directory: $SCRIPT_DIR"
|
||||
|
||||
# Define variables
|
||||
VENV_DIR=".venv"
|
||||
REQUIREMENTS="sqlmodel pydantic"
|
||||
|
||||
# Check if uv is installed
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Error: uv is not installed."
|
||||
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies using uv..."
|
||||
uv pip install $REQUIREMENTS
|
||||
|
||||
# Make example.py executable
|
||||
chmod +x example.py
|
||||
|
||||
# Remove existing database file if it exists
|
||||
if [ -f "business.db" ]; then
|
||||
echo "Removing existing database file..."
|
||||
rm business.db
|
||||
fi
|
||||
|
||||
# Run the example script
|
||||
echo "Running example script..."
|
||||
python example.py
|
||||
|
||||
echo "Done!"
|
297
herodb_old/src/models/py/models.py
Normal file
297
herodb_old/src/models/py/models.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Python port of the business models from Rust using SQLModel.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlmodel import Field, Relationship, SQLModel
|
||||
|
||||
|
||||
class SaleStatus(str, Enum):
|
||||
"""SaleStatus represents the status of a sale"""
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class ProductType(str, Enum):
|
||||
"""ProductType represents the type of a product"""
|
||||
PRODUCT = "product"
|
||||
SERVICE = "service"
|
||||
|
||||
|
||||
class ProductStatus(str, Enum):
|
||||
"""ProductStatus represents the status of a product"""
|
||||
AVAILABLE = "available"
|
||||
UNAVAILABLE = "unavailable"
|
||||
|
||||
|
||||
class Currency(SQLModel, table=True):
|
||||
"""Currency represents a monetary value with amount and currency code"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
amount: float
|
||||
currency_code: str
|
||||
|
||||
@classmethod
|
||||
def new(cls, amount: float, currency_code: str) -> "Currency":
|
||||
"""Create a new currency with amount and code"""
|
||||
return cls(amount=amount, currency_code=currency_code)
|
||||
|
||||
|
||||
class Customer(SQLModel, table=True):
|
||||
"""Customer represents a customer who can purchase products or services"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
pubkey: str
|
||||
contact_sids_json: str = Field(default="[]")
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
sales: List["Sale"] = Relationship(back_populates="customer")
|
||||
|
||||
@property
|
||||
def contact_sids(self) -> List[str]:
|
||||
"""Get the contact SIDs as a list"""
|
||||
return json.loads(self.contact_sids_json)
|
||||
|
||||
@contact_sids.setter
|
||||
def contact_sids(self, value: List[str]) -> None:
|
||||
"""Set the contact SIDs from a list"""
|
||||
self.contact_sids_json = json.dumps(value)
|
||||
|
||||
@classmethod
|
||||
def new(cls, name: str, description: str, pubkey: str, contact_sids: List[str] = None) -> "Customer":
|
||||
"""Create a new customer with default timestamps"""
|
||||
customer = cls(
|
||||
name=name,
|
||||
description=description,
|
||||
pubkey=pubkey,
|
||||
)
|
||||
if contact_sids:
|
||||
customer.contact_sids = contact_sids
|
||||
return customer
|
||||
|
||||
def add_contact(self, contact_id: int) -> None:
|
||||
"""Add a contact ID to the customer"""
|
||||
# In a real implementation, this would add a relationship to a Contact model
|
||||
# For simplicity, we're not implementing the Contact model in this example
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def add_contact_sid(self, circle_id: str, object_id: str) -> None:
|
||||
"""Add a smart ID (sid) to the customer's contact_sids list"""
|
||||
sid = f"{circle_id}_{object_id}"
|
||||
sids = self.contact_sids
|
||||
if sid not in sids:
|
||||
sids.append(sid)
|
||||
self.contact_sids = sids
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
|
||||
class ProductComponent(SQLModel, table=True):
|
||||
"""ProductComponent represents a component of a product"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
quantity: int
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||
product: Optional["Product"] = Relationship(back_populates="components")
|
||||
|
||||
@classmethod
|
||||
def new(cls, name: str, description: str, quantity: int) -> "ProductComponent":
|
||||
"""Create a new product component with default timestamps"""
|
||||
return cls(
|
||||
name=name,
|
||||
description=description,
|
||||
quantity=quantity,
|
||||
)
|
||||
|
||||
|
||||
class Product(SQLModel, table=True):
|
||||
"""Product represents a product or service offered"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
description: str
|
||||
type_: ProductType = Field(sa_column_kwargs={"name": "type"})
|
||||
category: str
|
||||
status: ProductStatus
|
||||
max_amount: int
|
||||
purchase_till: datetime
|
||||
active_till: datetime
|
||||
istemplate: bool = Field(default=False)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Price relationship
|
||||
price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
price: Optional[Currency] = Relationship()
|
||||
|
||||
# Relationships
|
||||
components: List[ProductComponent] = Relationship(back_populates="product")
|
||||
sale_items: List["SaleItem"] = Relationship(back_populates="product")
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
name: str,
|
||||
description: str,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: str,
|
||||
status: ProductStatus,
|
||||
max_amount: int,
|
||||
validity_days: int,
|
||||
istemplate: bool = False,
|
||||
) -> "Product":
|
||||
"""Create a new product with default timestamps"""
|
||||
now = datetime.utcnow()
|
||||
return cls(
|
||||
name=name,
|
||||
description=description,
|
||||
price=price,
|
||||
type_=type_,
|
||||
category=category,
|
||||
status=status,
|
||||
max_amount=max_amount,
|
||||
purchase_till=now + timedelta(days=365),
|
||||
active_till=now + timedelta(days=validity_days),
|
||||
istemplate=istemplate,
|
||||
)
|
||||
|
||||
def add_component(self, component: ProductComponent) -> None:
|
||||
"""Add a component to this product"""
|
||||
component.product = self
|
||||
self.components.append(component)
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def set_purchase_period(self, purchase_till: datetime) -> None:
|
||||
"""Update the purchase availability timeframe"""
|
||||
self.purchase_till = purchase_till
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def set_active_period(self, active_till: datetime) -> None:
|
||||
"""Update the active timeframe"""
|
||||
self.active_till = active_till
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def is_purchasable(self) -> bool:
|
||||
"""Check if the product is available for purchase"""
|
||||
return self.status == ProductStatus.AVAILABLE and datetime.utcnow() <= self.purchase_till
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if the product is still active (for services)"""
|
||||
return datetime.utcnow() <= self.active_till
|
||||
|
||||
|
||||
class SaleItem(SQLModel, table=True):
|
||||
"""SaleItem represents an item in a sale"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
quantity: int
|
||||
active_till: datetime
|
||||
|
||||
# Relationships
|
||||
sale_id: Optional[int] = Field(default=None, foreign_key="sale.id")
|
||||
sale: Optional["Sale"] = Relationship(back_populates="items")
|
||||
|
||||
product_id: Optional[int] = Field(default=None, foreign_key="product.id")
|
||||
product: Optional[Product] = Relationship(back_populates="sale_items")
|
||||
|
||||
unit_price_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
unit_price: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.unit_price_id]"})
|
||||
|
||||
subtotal_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
subtotal: Optional[Currency] = Relationship(sa_relationship_kwargs={"foreign_keys": "[SaleItem.subtotal_id]"})
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
product: Product,
|
||||
quantity: int,
|
||||
unit_price: Currency,
|
||||
active_till: datetime,
|
||||
) -> "SaleItem":
|
||||
"""Create a new sale item"""
|
||||
# Calculate subtotal
|
||||
amount = unit_price.amount * quantity
|
||||
subtotal = Currency(
|
||||
amount=amount,
|
||||
currency_code=unit_price.currency_code,
|
||||
)
|
||||
|
||||
return cls(
|
||||
name=product.name,
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
unit_price=unit_price,
|
||||
subtotal=subtotal,
|
||||
active_till=active_till,
|
||||
)
|
||||
|
||||
|
||||
class Sale(SQLModel, table=True):
|
||||
"""Sale represents a sale of products or services"""
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
status: SaleStatus
|
||||
sale_date: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
customer_id: Optional[int] = Field(default=None, foreign_key="customer.id")
|
||||
customer: Optional[Customer] = Relationship(back_populates="sales")
|
||||
|
||||
total_amount_id: Optional[int] = Field(default=None, foreign_key="currency.id")
|
||||
total_amount: Optional[Currency] = Relationship()
|
||||
|
||||
items: List[SaleItem] = Relationship(back_populates="sale")
|
||||
|
||||
@classmethod
|
||||
def new(
|
||||
cls,
|
||||
customer: Customer,
|
||||
currency_code: str,
|
||||
status: SaleStatus = SaleStatus.PENDING,
|
||||
) -> "Sale":
|
||||
"""Create a new sale with default timestamps"""
|
||||
total_amount = Currency(amount=0.0, currency_code=currency_code)
|
||||
|
||||
return cls(
|
||||
customer=customer,
|
||||
total_amount=total_amount,
|
||||
status=status,
|
||||
)
|
||||
|
||||
def add_item(self, item: SaleItem) -> None:
|
||||
"""Add an item to the sale and update the total amount"""
|
||||
item.sale = self
|
||||
|
||||
# Update the total amount
|
||||
if not self.items:
|
||||
# First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency(
|
||||
amount=item.subtotal.amount,
|
||||
currency_code=item.subtotal.currency_code,
|
||||
)
|
||||
else:
|
||||
# Add to the existing total
|
||||
# (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount
|
||||
|
||||
# Add the item to the list
|
||||
self.items.append(item)
|
||||
|
||||
# Update the sale timestamp
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
def update_status(self, status: SaleStatus) -> None:
|
||||
"""Update the status of the sale"""
|
||||
self.status = status
|
||||
self.updated_at = datetime.utcnow()
|
42
herodb_old/src/models/py/server.sh
Executable file
42
herodb_old/src/models/py/server.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# Script to start the FastAPI server
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Change to the directory where this script is located
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "Changed to directory: $SCRIPT_DIR"
|
||||
|
||||
# Define variables
|
||||
VENV_DIR=".venv"
|
||||
REQUIREMENTS="sqlmodel pydantic fastapi uvicorn"
|
||||
|
||||
# Check if uv is installed
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "Error: uv is not installed."
|
||||
echo "Please install it using: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create virtual environment if it doesn't exist
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating virtual environment..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies using uv..."
|
||||
uv pip install $REQUIREMENTS
|
||||
|
||||
# Make api.py executable
|
||||
chmod +x api.py
|
||||
|
||||
# Start the FastAPI server
|
||||
echo "Starting FastAPI server..."
|
||||
echo "API documentation available at: http://localhost:8000/docs"
|
||||
uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
Reference in New Issue
Block a user