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

View File

@@ -0,0 +1,530 @@
# Business Models
This directory contains the core business models used throughout the application for representing essential business objects like products, sales, and currency.
```
┌─────────────┐
│ Customer │
└──────┬──────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Currency │◄────┤ Product │◄────┤ │ │ │
└─────────────┘ └─────────────┘ │ │ │ │
▲ │ SaleItem │◄────┤ Sale │
│ │ │ │ │
┌─────┴──────────┐ │ │ │ │
│ProductComponent│ └─────────────┘ └──────┬──────┘
└────────────────┘ ▲ │
/ │
┌─────────────┐ ┌─────────────┐ / │
│ Currency │◄────┤ Service │◄────────/ │
└─────────────┘ └─────────────┘ │
┌─────────────┐ ┌─────────────┐
│ InvoiceItem │◄────┤ Invoice │
└─────────────┘ └─────────────┘
```
## Business Logic Relationships
- **Customer**: The entity purchasing products or services
- **Product/Service**: Defines what is being sold, including its base price
- Can be marked as a template (`is_template=true`) to create copies for actual sales
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
- Contains SaleItems that can be linked to either Products or Services
- **SaleItem**: Represents an item within a sale
- Can be linked to either a Product or a Service (via product_id or service_id)
- **Service**: Represents an ongoing service provided to a customer
- Created from a Product template when the product type is Service
- **Invoice**: Represents the billing document for a sale, with payment tracking
- Created from a Sale object to handle billing and payment tracking
## Root Objects
- Root objects are the ones stored directly in the DB
- Root Objects are:
- Customer
- Currency
- Product
- Sale
- Service
- Invoice
## Models
### Currency (Root Object)
Represents a monetary value with an amount and currency code.
**Properties:**
- `amount`: f64 - The monetary amount
- `currency_code`: String - The currency code (e.g., "USD", "EUR")
**Builder:**
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
### Customer (Root Object)
Represents a customer who can purchase products or services.
**Properties:**
- `id`: u32 - Unique identifier
- `name`: String - Customer name
- `description`: String - Customer description
- `pubkey`: String - Customer's public key
- `contact_ids`: Vec<u32> - List of contact IDs
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Methods:**
- `add_contact()` - Adds a contact ID to the customer
- `remove_contact()` - Removes a contact ID from the customer
### Product
#### ProductType Enum
Categorizes products:
- `Product` - Physical product
- `Service` - Non-physical service
#### ProductStatus Enum
Tracks product availability:
- `Available` - Product can be purchased
- `Unavailable` - Product cannot be purchased
#### ProductComponent
Represents a component part of a product.
**Properties:**
- `id`: u32 - Unique identifier
- `name`: String - Component name
- `description`: String - Component description
- `quantity`: i32 - Number of this component
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Builder:**
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
#### Product (Root Object)
Represents a product or service offered.
**Properties:**
- `id`: i64 - Unique identifier
- `name`: String - Product name
- `description`: String - Product description
- `price`: Currency - Product price
- `type_`: ProductType - Product or Service
- `category`: String - Product category
- `status`: ProductStatus - Available or Unavailable
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `max_amount`: i64 - Maximum quantity available
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
- `active_till`: DateTime<Utc> - When product/service expires
- `components`: Vec<ProductComponent> - List of product components
- `is_template`: bool - Whether this is a template product (to be added)
**Methods:**
- `add_component()` - Adds a component to this product
- `set_purchase_period()` - Updates purchase availability timeframe
- `set_active_period()` - Updates active timeframe
- `is_purchasable()` - Checks if product is available for purchase
- `is_active()` - Checks if product is still active
**Builder:**
- `ProductBuilder` - Provides a fluent interface for creating Product instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
- `get_id()` - Returns the ID as a string
- `db_prefix()` - Returns "product" as the database prefix
### Service (Root Object)
#### BillingFrequency Enum
Defines how often a service is billed:
- `Hourly` - Billed by the hour
- `Daily` - Billed daily
- `Weekly` - Billed weekly
- `Monthly` - Billed monthly
- `Yearly` - Billed yearly
#### ServiceStatus Enum
Tracks the status of a service:
- `Active` - Service is currently active
- `Paused` - Service is temporarily paused
- `Cancelled` - Service has been cancelled
- `Completed` - Service has been completed
#### ServiceItem
Represents an item within a service.
**Properties:**
- `id`: u32 - Unique identifier
- `service_id`: u32 - Parent service ID
- `product_id`: u32 - ID of the product this service is based on
- `name`: String - Service name
- `description`: String - Detailed description of the service item
- `comments`: String - Additional notes or comments about the service item
- `quantity`: i32 - Number of units
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price before tax
- `tax_rate`: f64 - Tax rate as a percentage
- `tax_amount`: Currency - Calculated tax amount
- `is_taxable`: bool - Whether this item is taxable
- `active_till`: DateTime<Utc> - When service expires
#### Service
Represents an ongoing service provided to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer receiving the service
- `total_amount`: Currency - Total service amount including tax
- `status`: ServiceStatus - Current service status
- `billing_frequency`: BillingFrequency - How often the service is billed
- `service_date`: DateTime<Utc> - When service started
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<ServiceItem> - List of items in the service
- `is_template`: bool - Whether this is a template service (to be added)
**Methods:**
- `add_item()` - Adds an item to the service and updates total
- `calculate_total()` - Recalculates the total amount
- `update_status()` - Updates the status of the service
### Sale
#### SaleStatus Enum
Tracks the status of a sale:
- `Pending` - Sale is in progress
- `Completed` - Sale has been finalized
- `Cancelled` - Sale has been cancelled
#### SaleItem
Represents an item within a sale.
**Properties:**
- `id`: u32 - Unique identifier
- `sale_id`: u32 - Parent sale ID
- `product_id`: Option<u32> - ID of the product sold (if this is a product sale)
- `service_id`: Option<u32> - ID of the service sold (if this is a service sale)
- `name`: String - Product/service name at time of sale
- `description`: String - Detailed description of the item
- `comments`: String - Additional notes or comments about the item
- `quantity`: i32 - Number of items purchased
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price for this item before tax (calculated)
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
- `tax_amount`: Currency - Calculated tax amount for this item
- `active_till`: DateTime<Utc> - When item/service expires
**Methods:**
- `total_with_tax()` - Returns the total amount including tax
**Builder:**
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
#### Sale (Root Object)
Represents a complete sale transaction.
**Properties:**
- `id`: u32 - Unique identifier
- `company_id`: u32 - ID of the company making the sale
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
- `buyer_name`: String - Name of the buyer
- `buyer_email`: String - Email of the buyer
- `subtotal_amount`: Currency - Total sale amount before tax
- `tax_amount`: Currency - Total tax amount for the sale
- `total_amount`: Currency - Total sale amount including tax
- `status`: SaleStatus - Current sale status
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
- `sale_date`: DateTime<Utc> - When sale occurred
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<SaleItem> - List of items in the sale
**Methods:**
- `add_item()` - Adds an item to the sale and updates totals
- `update_status()` - Updates the status of the sale
- `recalculate_totals()` - Recalculates all totals based on items
- `create_service()` - Creates a service from this sale (to be added)
**Builder:**
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
- `get_id()` - Returns the ID as a string
- `db_prefix()` - Returns "sale" as the database prefix
## Usage Examples
### Creating a Currency
```rust
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
```
### Creating a Product
```rust
// Create a currency using the builder
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Create a component using the builder
let component = ProductComponentBuilder::new()
.id(1)
.name("Basic Support")
.description("24/7 email support")
.quantity(1)
.build()
.expect("Failed to build product component");
// Create a product using the builder
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(price)
.type_(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.add_component(component)
.build()
.expect("Failed to build product");
```
### Creating a Sale
```rust
let now = Utc::now();
// Create a currency using the builder
let unit_price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Create a sale item using the builder
let item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.product_id(1)
.name("Premium Service")
.quantity(1)
.unit_price(unit_price)
.tax_rate(20.0) // 20% tax rate
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
// Create a sale using the builder
let mut sale = SaleBuilder::new()
.id(1)
.company_id(101)
.buyer_name("John Doe")
.buyer_email("john.doe@example.com")
.currency_code("USD")
.status(SaleStatus::Pending)
.add_item(item)
.build()
.expect("Failed to build sale");
// Update the sale status
sale.update_status(SaleStatus::Completed);
// The sale now contains:
// - subtotal_amount: The sum of all items before tax
// - tax_amount: The sum of all tax amounts
// - total_amount: The total including tax
```
### Relationship Between Sale and Invoice
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
```rust
// Create an invoice item linked to a sale
let invoice_item = InvoiceItemBuilder::new()
.id(1)
.invoice_id(1)
.description("Premium Service")
.amount(sale.total_amount.clone()) // Use the total amount from the sale
.sale_id(sale.id) // Link to the sale
.build()
.expect("Failed to build invoice item");
```
## Database Operations
The library provides model-specific convenience methods for common database operations:
```rust
// Insert a product
db.insert_product(&product).expect("Failed to insert product");
// Retrieve a product by ID
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
// List all products
let all_products = db.list_products().expect("Failed to list products");
// Delete a product
db.delete_product(1).expect("Failed to delete product");
```
These methods are available for all root objects:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
### Invoice (Root Object)
#### InvoiceStatus Enum
Tracks the status of an invoice:
- `Draft` - Invoice is in draft state
- `Sent` - Invoice has been sent to the customer
- `Paid` - Invoice has been paid
- `Overdue` - Invoice is past due date
- `Cancelled` - Invoice has been cancelled
#### PaymentStatus Enum
Tracks the payment status of an invoice:
- `Unpaid` - Invoice has not been paid
- `PartiallyPaid` - Invoice has been partially paid
- `Paid` - Invoice has been fully paid
#### Payment
Represents a payment made against an invoice.
**Properties:**
- `amount`: Currency - Payment amount
- `date`: DateTime<Utc> - Payment date
- `method`: String - Payment method
- `comment`: String - Payment comment
#### InvoiceItem
Represents an item in an invoice.
**Properties:**
- `id`: u32 - Unique identifier
- `invoice_id`: u32 - Parent invoice ID
- `description`: String - Item description
- `amount`: Currency - Item amount
- `service_id`: Option<u32> - ID of the service this item is for
- `sale_id`: Option<u32> - ID of the sale this item is for
**Methods:**
- `link_to_service()` - Links the invoice item to a service
- `link_to_sale()` - Links the invoice item to a sale
#### Invoice
Represents an invoice sent to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer being invoiced
- `total_amount`: Currency - Total invoice amount
- `balance_due`: Currency - Amount still due
- `status`: InvoiceStatus - Current invoice status
- `payment_status`: PaymentStatus - Current payment status
- `issue_date`: DateTime<Utc> - When invoice was issued
- `due_date`: DateTime<Utc> - When payment is due
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<InvoiceItem> - List of items in the invoice
- `payments`: Vec<Payment> - List of payments made
**Methods:**
- `add_item()` - Adds an item to the invoice
- `calculate_total()` - Calculates the total amount
- `add_payment()` - Adds a payment to the invoice
- `calculate_balance()` - Calculates the balance due
- `update_payment_status()` - Updates the payment status
- `update_status()` - Updates the status of the invoice
- `is_overdue()` - Checks if the invoice is overdue
- `check_if_overdue()` - Marks the invoice as overdue if past due date
### Relationships Between Models
#### Product/Service Templates and Instances
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
#### Sale to Service Relationship
A SaleItem can be directly linked to a Service via the `service_id` field. This allows for selling existing services or creating new services as part of a sale:
```rust
// Create a SaleItem linked to a service
let sale_item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.service_id(Some(42)) // Link to service with ID 42
.product_id(None) // No product link since this is a service
.name("Premium Support")
.quantity(1)
.unit_price(unit_price)
.tax_rate(20.0)
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
```
#### Sale to Invoice Relationship
An Invoice is created from a Sale to handle billing and payment tracking:
```rust
// Create an invoice from a sale
let invoice = Invoice::from_sale(
invoice_id,
sale,
due_date
);
```
#### Customer-Centric View
The models allow tracking all customer interactions:
- What products/services they've purchased (via Sale records)
- What ongoing services they have (via Service records)
- What they've been invoiced for (via Invoice records)
- What they've paid (via Payment records in Invoices)
```rust
// Get all sales for a customer
let customer_sales = db.list_sales_by_customer(customer_id);
// Get all services for a customer
let customer_services = db.list_services_by_customer(customer_id);
// Get all invoices for a customer
let customer_invoices = db.list_invoices_by_customer(customer_id);
```

View File

@@ -0,0 +1,293 @@
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// ContractStatus represents the status of a contract
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContractStatus {
Active,
Expired,
Terminated,
}
/// Contract represents a legal agreement between a customer and the business
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub id: u32,
pub customer_id: u32,
pub service_id: Option<u32>,
pub sale_id: Option<u32>,
pub terms: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub auto_renewal: bool,
pub renewal_terms: String,
pub status: ContractStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Contract {
/// Create a new contract with default timestamps
pub fn new(
id: u32,
customer_id: u32,
terms: String,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
auto_renewal: bool,
renewal_terms: String,
) -> Self {
let now = Utc::now();
Self {
id,
customer_id,
service_id: None,
sale_id: None,
terms,
start_date,
end_date,
auto_renewal,
renewal_terms,
status: ContractStatus::Active,
created_at: now,
updated_at: now,
}
}
/// Link the contract to a service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.sale_id = None; // A contract can only be linked to either a service or a sale
self.updated_at = Utc::now();
}
/// Link the contract to a sale
pub fn link_to_sale(&mut self, sale_id: u32) {
self.sale_id = Some(sale_id);
self.service_id = None; // A contract can only be linked to either a service or a sale
self.updated_at = Utc::now();
}
/// Check if the contract is currently active
pub fn is_active(&self) -> bool {
let now = Utc::now();
self.status == ContractStatus::Active &&
now >= self.start_date &&
now <= self.end_date
}
/// Check if the contract has expired
pub fn is_expired(&self) -> bool {
let now = Utc::now();
now > self.end_date
}
/// Update the contract status
pub fn update_status(&mut self, status: ContractStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Renew the contract based on renewal terms
pub fn renew(&mut self) -> Result<(), &'static str> {
if !self.auto_renewal {
return Err("Contract is not set for auto-renewal");
}
if self.status != ContractStatus::Active {
return Err("Cannot renew a non-active contract");
}
// Calculate new dates based on the current end date
let duration = self.end_date - self.start_date;
self.start_date = self.end_date;
self.end_date = self.end_date + duration;
self.updated_at = Utc::now();
Ok(())
}
}
/// Builder for Contract
pub struct ContractBuilder {
id: Option<u32>,
customer_id: Option<u32>,
service_id: Option<u32>,
sale_id: Option<u32>,
terms: Option<String>,
start_date: Option<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
auto_renewal: Option<bool>,
renewal_terms: Option<String>,
status: Option<ContractStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl ContractBuilder {
/// Create a new ContractBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
service_id: None,
sale_id: None,
terms: None,
start_date: None,
end_date: None,
auto_renewal: None,
renewal_terms: None,
status: None,
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self.sale_id = None; // A contract can only be linked to either a service or a sale
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self.service_id = None; // A contract can only be linked to either a service or a sale
self
}
/// Set the terms
pub fn terms<S: Into<String>>(mut self, terms: S) -> Self {
self.terms = Some(terms.into());
self
}
/// Set the start_date
pub fn start_date(mut self, start_date: DateTime<Utc>) -> Self {
self.start_date = Some(start_date);
self
}
/// Set the end_date
pub fn end_date(mut self, end_date: DateTime<Utc>) -> Self {
self.end_date = Some(end_date);
self
}
/// Set auto_renewal
pub fn auto_renewal(mut self, auto_renewal: bool) -> Self {
self.auto_renewal = Some(auto_renewal);
self
}
/// Set the renewal_terms
pub fn renewal_terms<S: Into<String>>(mut self, renewal_terms: S) -> Self {
self.renewal_terms = Some(renewal_terms.into());
self
}
/// Set the status
pub fn status(mut self, status: ContractStatus) -> Self {
self.status = Some(status);
self
}
/// Build the Contract object
pub fn build(self) -> Result<Contract, &'static str> {
let now = Utc::now();
// Validate that start_date is before end_date
let start_date = self.start_date.ok_or("start_date is required")?;
let end_date = self.end_date.ok_or("end_date is required")?;
if start_date >= end_date {
return Err("start_date must be before end_date");
}
Ok(Contract {
id: self.id.ok_or("id is required")?,
customer_id: self.customer_id.ok_or("customer_id is required")?,
service_id: self.service_id,
sale_id: self.sale_id,
terms: self.terms.ok_or("terms is required")?,
start_date,
end_date,
auto_renewal: self.auto_renewal.unwrap_or(false),
renewal_terms: self.renewal_terms.ok_or("renewal_terms is required")?,
status: self.status.unwrap_or(ContractStatus::Active),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
impl Storable for Contract {}
// Implement Model trait
impl Model for Contract {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"contract"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
// Add an index for service_id if present
if let Some(service_id) = self.service_id {
keys.push(IndexKey {
name: "service_id",
value: service_id.to_string(),
});
}
// Add an index for sale_id if present
if let Some(sale_id) = self.sale_id {
keys.push(IndexKey {
name: "sale_id",
value: sale_id.to_string(),
});
}
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for active contracts
if self.is_active() {
keys.push(IndexKey {
name: "active",
value: "true".to_string(),
});
}
keys
}
}

View File

@@ -0,0 +1,114 @@
use crate::db::model::{Model, IndexKey};
use crate::db::{Storable, DbError, DbResult};
use chrono::{DateTime, Duration, Utc};
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Currency {
pub id: u32,
pub amount: f64,
pub currency_code: String,
}
impl Currency {
/// Create a new currency with amount and code
pub fn new(id: u32, amount: f64, currency_code: String) -> Self {
Self {
id,
amount,
currency_code,
}
}
pub fn amount(&mut self) -> f64 {
self.amount
}
}
/// Builder for Currency
#[derive(Clone, CustomType)]
pub struct CurrencyBuilder {
id: Option<u32>,
amount: Option<f64>,
currency_code: Option<String>,
}
impl CurrencyBuilder {
/// Create a new CurrencyBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
amount: None,
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the amount
pub fn amount(mut self, amount: f64) -> Self {
self.amount = Some(amount);
self
}
/// Set the currency code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Build the Currency object
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
Ok(Currency {
id: self.id.ok_or("id is required")?,
amount: self.amount.ok_or("amount is required")?,
currency_code: self.currency_code.ok_or("currency_code is required")?,
})
}
}
// Implement Storable trait
impl Storable for Currency {}
// Implement Model trait
impl Model for Currency {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"currency"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for currency_code
keys.push(IndexKey {
name: "currency_code",
value: self.currency_code.clone(),
});
// Add an index for amount range
// This allows finding currencies within specific ranges
let amount_range = match self.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "amount_range",
value: amount_range.to_string(),
});
keys
}
}

View File

@@ -0,0 +1,166 @@
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Customer represents a customer who can purchase products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
pub id: u32,
pub name: String,
pub description: String,
pub pubkey: String,
pub contact_ids: Vec<u32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Customer {
/// Create a new customer with default timestamps
pub fn new(
id: u32,
name: String,
description: String,
pubkey: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
description,
pubkey,
contact_ids: Vec::new(),
created_at: now,
updated_at: now,
}
}
/// Add a contact ID to the customer
pub fn add_contact(&mut self, contact_id: u32) {
if !self.contact_ids.contains(&contact_id) {
self.contact_ids.push(contact_id);
self.updated_at = Utc::now();
}
}
/// Remove a contact ID from the customer
pub fn remove_contact(&mut self, contact_id: u32) -> bool {
let len = self.contact_ids.len();
self.contact_ids.retain(|&id| id != contact_id);
if self.contact_ids.len() < len {
self.updated_at = Utc::now();
true
} else {
false
}
}
}
/// Builder for Customer
pub struct CustomerBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
pubkey: Option<String>,
contact_ids: Vec<u32>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl CustomerBuilder {
/// Create a new CustomerBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
pubkey: None,
contact_ids: Vec::new(),
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the pubkey
pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
self.pubkey = Some(pubkey.into());
self
}
/// Add a contact ID
pub fn add_contact(mut self, contact_id: u32) -> Self {
self.contact_ids.push(contact_id);
self
}
/// Set multiple contact IDs
pub fn contact_ids(mut self, contact_ids: Vec<u32>) -> Self {
self.contact_ids = contact_ids;
self
}
/// Build the Customer object
pub fn build(self) -> Result<Customer, &'static str> {
let now = Utc::now();
Ok(Customer {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
pubkey: self.pubkey.ok_or("pubkey is required")?,
contact_ids: self.contact_ids,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
impl Storable for Customer {}
// Implement Model trait
impl Model for Customer {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"customer"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for the name
keys.push(IndexKey {
name: "name",
value: self.name.clone(),
});
// Add an index for the pubkey
keys.push(IndexKey {
name: "pubkey",
value: self.pubkey.clone(),
});
keys
}
}

View File

@@ -0,0 +1,178 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::model::{Model, Storable};
/// ExchangeRate represents an exchange rate between two currencies
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeRate {
pub id: u32,
pub base_currency: String,
pub target_currency: String,
pub rate: f64,
pub timestamp: DateTime<Utc>,
}
impl ExchangeRate {
/// Create a new exchange rate
pub fn new(id: u32, base_currency: String, target_currency: String, rate: f64) -> Self {
Self {
id,
base_currency,
target_currency,
rate,
timestamp: Utc::now(),
}
}
}
/// Builder for ExchangeRate
pub struct ExchangeRateBuilder {
id: Option<u32>,
base_currency: Option<String>,
target_currency: Option<String>,
rate: Option<f64>,
timestamp: Option<DateTime<Utc>>,
}
impl ExchangeRateBuilder {
/// Create a new ExchangeRateBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
base_currency: None,
target_currency: None,
rate: None,
timestamp: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the base currency
pub fn base_currency<S: Into<String>>(mut self, base_currency: S) -> Self {
self.base_currency = Some(base_currency.into());
self
}
/// Set the target currency
pub fn target_currency<S: Into<String>>(mut self, target_currency: S) -> Self {
self.target_currency = Some(target_currency.into());
self
}
/// Set the rate
pub fn rate(mut self, rate: f64) -> Self {
self.rate = Some(rate);
self
}
/// Set the timestamp
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
/// Build the ExchangeRate object
pub fn build(self) -> Result<ExchangeRate, &'static str> {
let now = Utc::now();
Ok(ExchangeRate {
id: self.id.ok_or("id is required")?,
base_currency: self.base_currency.ok_or("base_currency is required")?,
target_currency: self.target_currency.ok_or("target_currency is required")?,
rate: self.rate.ok_or("rate is required")?,
timestamp: self.timestamp.unwrap_or(now),
})
}
}
impl Storable for ExchangeRate {}
// Implement Model trait
impl Model for ExchangeRate {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"exchange_rate"
}
}
/// ExchangeRateService provides methods to get and set exchange rates
#[derive(Clone)]
pub struct ExchangeRateService {
rates: Arc<Mutex<HashMap<String, ExchangeRate>>>,
}
impl ExchangeRateService {
/// Create a new exchange rate service
pub fn new() -> Self {
Self {
rates: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Set an exchange rate
pub fn set_rate(&self, exchange_rate: ExchangeRate) {
let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency);
let mut rates = self.rates.lock().unwrap();
rates.insert(key, exchange_rate);
}
/// Get an exchange rate
pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option<ExchangeRate> {
let key = format!("{}_{}", base_currency, target_currency);
let rates = self.rates.lock().unwrap();
rates.get(&key).cloned()
}
/// Convert an amount from one currency to another
pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option<f64> {
// If the currencies are the same, return the amount
if from_currency == to_currency {
return Some(amount);
}
// Try to get the direct exchange rate
if let Some(rate) = self.get_rate(from_currency, to_currency) {
return Some(amount * rate.rate);
}
// Try to get the inverse exchange rate
if let Some(rate) = self.get_rate(to_currency, from_currency) {
return Some(amount / rate.rate);
}
// Try to convert via USD
if from_currency != "USD" && to_currency != "USD" {
if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") {
return self.convert(from_to_usd, "USD", to_currency);
}
}
None
}
}
// Create a global instance of the exchange rate service
lazy_static::lazy_static! {
pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = {
let service = ExchangeRateService::new();
// Set some default exchange rates
service.set_rate(ExchangeRate::new(1, "USD".to_string(), "EUR".to_string(), 0.85));
service.set_rate(ExchangeRate::new(2, "USD".to_string(), "GBP".to_string(), 0.75));
service.set_rate(ExchangeRate::new(3, "USD".to_string(), "JPY".to_string(), 110.0));
service.set_rate(ExchangeRate::new(4, "USD".to_string(), "CAD".to_string(), 1.25));
service.set_rate(ExchangeRate::new(5, "USD".to_string(), "AUD".to_string(), 1.35));
service
};
}

View File

@@ -0,0 +1,577 @@
use crate::models::biz::Currency; // Use crate:: for importing from the module
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc, Datelike};
use serde::{Deserialize, Serialize};
/// InvoiceStatus represents the status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum InvoiceStatus {
Draft,
Sent,
Paid,
Overdue,
Cancelled,
}
/// PaymentStatus represents the payment status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PaymentStatus {
Unpaid,
PartiallyPaid,
Paid,
}
/// Payment represents a payment made against an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub amount: Currency,
pub date: DateTime<Utc>,
pub method: String,
pub comment: String,
}
impl Payment {
/// Create a new payment
pub fn new(amount: Currency, method: String, comment: String) -> Self {
Self {
amount,
date: Utc::now(),
method,
comment,
}
}
}
/// InvoiceItem represents an item in an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceItem {
pub id: u32,
pub invoice_id: u32,
pub description: String,
pub amount: Currency,
pub service_id: Option<u32>,
pub sale_id: Option<u32>,
}
impl InvoiceItem {
/// Create a new invoice item
pub fn new(
id: u32,
invoice_id: u32,
description: String,
amount: Currency,
) -> Self {
Self {
id,
invoice_id,
description,
amount,
service_id: None,
sale_id: None,
}
}
/// Link the invoice item to a service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
}
/// Link the invoice item to a sale
pub fn link_to_sale(&mut self, sale_id: u32) {
self.sale_id = Some(sale_id);
self.service_id = None; // An invoice item can only be linked to either a service or a sale
}
}
/// Builder for InvoiceItem
pub struct InvoiceItemBuilder {
id: Option<u32>,
invoice_id: Option<u32>,
description: Option<String>,
amount: Option<Currency>,
service_id: Option<u32>,
sale_id: Option<u32>,
}
impl InvoiceItemBuilder {
/// Create a new InvoiceItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
invoice_id: None,
description: None,
amount: None,
service_id: None,
sale_id: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the invoice_id
pub fn invoice_id(mut self, invoice_id: u32) -> Self {
self.invoice_id = Some(invoice_id);
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the amount
pub fn amount(mut self, amount: Currency) -> Self {
self.amount = Some(amount);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self.service_id = None; // An invoice item can only be linked to either a service or a sale
self
}
/// Build the InvoiceItem object
pub fn build(self) -> Result<InvoiceItem, &'static str> {
Ok(InvoiceItem {
id: self.id.ok_or("id is required")?,
invoice_id: self.invoice_id.ok_or("invoice_id is required")?,
description: self.description.ok_or("description is required")?,
amount: self.amount.ok_or("amount is required")?,
service_id: self.service_id,
sale_id: self.sale_id,
})
}
}
/// Invoice represents an invoice sent to a customer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: u32,
pub customer_id: u32,
pub total_amount: Currency,
pub balance_due: Currency,
pub status: InvoiceStatus,
pub payment_status: PaymentStatus,
pub issue_date: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<InvoiceItem>,
pub payments: Vec<Payment>,
}
impl Invoice {
/// Create a new invoice with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
issue_date: DateTime<Utc>,
due_date: DateTime<Utc>,
) -> Self {
let now = Utc::now();
let zero_amount = Currency::new(
0, // Use 0 as a temporary ID for zero amounts
0.0,
currency_code.clone()
);
Self {
id,
customer_id,
total_amount: zero_amount.clone(),
balance_due: zero_amount,
status: InvoiceStatus::Draft,
payment_status: PaymentStatus::Unpaid,
issue_date,
due_date,
created_at: now,
updated_at: now,
items: Vec::new(),
payments: Vec::new(),
}
}
/// Add an item to the invoice and update the total amount
pub fn add_item(&mut self, item: InvoiceItem) {
// Make sure the item's invoice_id matches this invoice
assert_eq!(self.id, item.invoice_id, "Item invoice_id must match invoice id");
// Update the total amount
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.amount.amount,
item.amount.currency_code.clone()
);
self.balance_due = Currency::new(
0, // Use 0 as a temporary ID
item.amount.amount,
item.amount.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
self.total_amount.amount += item.amount.amount;
self.balance_due.amount += item.amount.amount;
}
// Add the item to the list
self.items.push(item);
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Calculate the total amount based on all items
pub fn calculate_total(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].amount.currency_code.clone();
// Calculate the total amount
let mut total = 0.0;
for item in &self.items {
total += item.amount.amount;
}
// Update the total amount
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
total,
currency_code.clone()
);
// Recalculate the balance due
self.calculate_balance();
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Add a payment to the invoice and update the balance due and payment status
pub fn add_payment(&mut self, payment: Payment) {
// Update the balance due
self.balance_due.amount -= payment.amount.amount;
// Add the payment to the list
self.payments.push(payment);
// Update the payment status
self.update_payment_status();
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Calculate the balance due based on total amount and payments
pub fn calculate_balance(&mut self) {
// Start with the total amount
let mut balance = self.total_amount.amount;
// Subtract all payments
for payment in &self.payments {
balance -= payment.amount.amount;
}
// Update the balance due
self.balance_due = Currency::new(
0, // Use 0 as a temporary ID
balance,
self.total_amount.currency_code.clone()
);
// Update the payment status
self.update_payment_status();
}
/// Update the payment status based on the balance due
fn update_payment_status(&mut self) {
if self.balance_due.amount <= 0.0 {
self.payment_status = PaymentStatus::Paid;
// If fully paid, also update the invoice status
if self.status != InvoiceStatus::Cancelled {
self.status = InvoiceStatus::Paid;
}
} else if self.payments.is_empty() {
self.payment_status = PaymentStatus::Unpaid;
} else {
self.payment_status = PaymentStatus::PartiallyPaid;
}
}
/// Update the status of the invoice
pub fn update_status(&mut self, status: InvoiceStatus) {
self.status = status;
self.updated_at = Utc::now();
// If the invoice is cancelled, don't change the payment status
if status != InvoiceStatus::Cancelled {
// Re-evaluate the payment status
self.update_payment_status();
}
}
/// Check if the invoice is overdue
pub fn is_overdue(&self) -> bool {
let now = Utc::now();
self.payment_status != PaymentStatus::Paid &&
now > self.due_date &&
self.status != InvoiceStatus::Cancelled
}
/// Mark the invoice as overdue if it's past the due date
pub fn check_if_overdue(&mut self) -> bool {
if self.is_overdue() && self.status != InvoiceStatus::Overdue {
self.status = InvoiceStatus::Overdue;
self.updated_at = Utc::now();
true
} else {
false
}
}
}
/// Builder for Invoice
pub struct InvoiceBuilder {
id: Option<u32>,
customer_id: Option<u32>,
total_amount: Option<Currency>,
balance_due: Option<Currency>,
status: Option<InvoiceStatus>,
payment_status: Option<PaymentStatus>,
issue_date: Option<DateTime<Utc>>,
due_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<InvoiceItem>,
payments: Vec<Payment>,
currency_code: Option<String>,
}
impl InvoiceBuilder {
/// Create a new InvoiceBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
total_amount: None,
balance_due: None,
status: None,
payment_status: None,
issue_date: None,
due_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
payments: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: InvoiceStatus) -> Self {
self.status = Some(status);
self
}
/// Set the issue_date
pub fn issue_date(mut self, issue_date: DateTime<Utc>) -> Self {
self.issue_date = Some(issue_date);
self
}
/// Set the due_date
pub fn due_date(mut self, due_date: DateTime<Utc>) -> Self {
self.due_date = Some(due_date);
self
}
/// Add an item to the invoice
pub fn add_item(mut self, item: InvoiceItem) -> Self {
self.items.push(item);
self
}
/// Add a payment to the invoice
pub fn add_payment(mut self, payment: Payment) -> Self {
self.payments.push(payment);
self
}
/// Build the Invoice object
pub fn build(self) -> Result<Invoice, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty total amount and balance due
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate total amount from items
for item in &self.items {
// Make sure the item's invoice_id matches this invoice
if item.invoice_id != id {
return Err("Item invoice_id must match invoice id");
}
total_amount.amount += item.amount.amount;
}
// Calculate balance due (total minus payments)
let mut balance_due = total_amount.clone();
for payment in &self.payments {
balance_due.amount -= payment.amount.amount;
}
// Determine payment status
let payment_status = if balance_due.amount <= 0.0 {
PaymentStatus::Paid
} else if self.payments.is_empty() {
PaymentStatus::Unpaid
} else {
PaymentStatus::PartiallyPaid
};
// Determine invoice status if not provided
let status = if let Some(status) = self.status {
status
} else if payment_status == PaymentStatus::Paid {
InvoiceStatus::Paid
} else {
InvoiceStatus::Draft
};
Ok(Invoice {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
total_amount: self.total_amount.unwrap_or(total_amount),
balance_due: self.balance_due.unwrap_or(balance_due),
status,
payment_status,
issue_date: self.issue_date.ok_or("issue_date is required")?,
due_date: self.due_date.ok_or("due_date is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
payments: self.payments,
})
}
}
impl Storable for Invoice {}
// Implement Model trait
impl Model for Invoice {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"invoice"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for payment_status
keys.push(IndexKey {
name: "payment_status",
value: format!("{:?}", self.payment_status),
});
// Add an index for currency code
keys.push(IndexKey {
name: "currency",
value: self.total_amount.currency_code.clone(),
});
// Add an index for amount range
let amount_range = match self.total_amount.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "amount_range",
value: amount_range.to_string(),
});
// Add an index for issue date (year-month)
keys.push(IndexKey {
name: "issue_date",
value: format!("{}", self.issue_date.format("%Y-%m")),
});
// Add an index for due date (year-month)
keys.push(IndexKey {
name: "due_date",
value: format!("{}", self.due_date.format("%Y-%m")),
});
// Add an index for overdue invoices
if self.is_overdue() {
keys.push(IndexKey {
name: "overdue",
value: "true".to_string(),
});
}
keys
}
}

View File

@@ -0,0 +1,30 @@
pub mod user;
pub mod vote;
pub mod company;
pub mod meeting;
pub mod product;
pub mod sale;
pub mod shareholder;
// pub mod db; // Moved to src/zaz/db
// pub mod migration; // Removed
// Re-export all model types for convenience
pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use company::{Company, CompanyStatus, BusinessType};
pub use meeting::Meeting;
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
// Re-export Currency and its builder
pub use product::Currency;
pub use currency::CurrencyBuilder;
// Re-export database components
// Re-export database components from db module
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};

View File

@@ -0,0 +1,28 @@
pub mod currency;
pub mod product;
pub mod sale;
pub mod exchange_rate;
pub mod service;
pub mod customer;
pub mod contract;
pub mod invoice;
// Re-export all model types for convenience
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::{Sale, SaleItem, SaleStatus};
pub use currency::Currency;
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency};
pub use customer::Customer;
pub use contract::{Contract, ContractStatus};
pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment};
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
pub use currency::CurrencyBuilder;
pub use exchange_rate::ExchangeRateBuilder;
pub use service::{ServiceBuilder, ServiceItemBuilder};
pub use customer::CustomerBuilder;
pub use contract::ContractBuilder;
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};

View File

@@ -0,0 +1,425 @@
use crate::db::model::{Model, IndexKey};
use crate::db::Storable;
use chrono::{DateTime, Duration, Utc};
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
use serde::{Deserialize, Serialize};
/// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductType {
Product,
Service,
}
/// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductStatus {
Available,
Unavailable,
}
/// ProductComponent represents a component of a product
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductComponent {
pub id: u32,
pub name: String,
pub description: String,
pub quantity: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ProductComponent {
/// Create a new product component with default timestamps
pub fn new(id: u32, name: String, description: String, quantity: i64) -> Self {
let now = Utc::now();
Self {
id,
name,
description,
quantity,
created_at: now,
updated_at: now,
}
}
}
/// Builder for ProductComponent
#[derive(Clone, CustomType)]
pub struct ProductComponentBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
quantity: Option<i64>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl ProductComponentBuilder {
/// Create a new ProductComponentBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
quantity: None,
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i64) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the created_at timestamp
pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
self.created_at = Some(created_at);
self
}
/// Set the updated_at timestamp
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
self.updated_at = Some(updated_at);
self
}
/// Build the ProductComponent object
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
let now = Utc::now();
Ok(ProductComponent {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
quantity: self.quantity.ok_or("quantity is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
/// Product represents a product or service offered in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
pub id: u32,
pub name: String,
pub description: String,
pub price: Currency,
pub type_: ProductType,
pub category: String,
pub status: ProductStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub max_amount: i64, // means allows us to define how many max of this there are
pub purchase_till: DateTime<Utc>,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
pub components: Vec<ProductComponent>,
}
impl Product {
/// Create a new product with default timestamps
pub fn new(
id: u32,
name: String,
description: String,
price: Currency,
type_: ProductType,
category: String,
status: ProductStatus,
max_amount: i64,
validity_days: i64, // How many days the product is valid after purchase
) -> Self {
let now = Utc::now();
// Default: purchasable for 1 year, active for specified validity days after purchase
Self {
id,
name,
description,
price,
type_,
category,
status,
created_at: now,
updated_at: now,
max_amount,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(validity_days),
components: Vec::new(),
}
}
/// Add a component to this product
pub fn add_component(&mut self, component: ProductComponent) {
self.components.push(component);
self.updated_at = Utc::now();
}
/// Update the purchase availability timeframe
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
self.purchase_till = purchase_till;
self.updated_at = Utc::now();
}
/// Update the active timeframe
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
self.active_till = active_till;
self.updated_at = Utc::now();
}
/// Check if the product is available for purchase
pub fn is_purchasable(&self) -> bool {
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
}
/// Check if the product is still active (for services)
pub fn is_active(&self) -> bool {
Utc::now() <= self.active_till
}
}
/// Builder for Product
#[derive(Clone, CustomType)]
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<Currency>,
type_: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
max_amount: Option<i64>,
purchase_till: Option<DateTime<Utc>>,
active_till: Option<DateTime<Utc>>,
components: Vec<ProductComponent>,
validity_days: Option<i64>,
}
impl ProductBuilder {
/// Create a new ProductBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
price: None,
type_: None,
category: None,
status: None,
created_at: None,
updated_at: None,
max_amount: None,
purchase_till: None,
active_till: None,
components: Vec::new(),
validity_days: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the price
pub fn price(mut self, price: Currency) -> Self {
self.price = Some(price);
self
}
/// Set the product type
pub fn type_(mut self, type_: ProductType) -> Self {
self.type_ = Some(type_);
self
}
/// Set the category
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
/// Set the status
pub fn status(mut self, status: ProductStatus) -> Self {
self.status = Some(status);
self
}
/// Set the max amount
pub fn max_amount(mut self, max_amount: i64) -> Self {
self.max_amount = Some(max_amount);
self
}
/// Set the validity days
pub fn validity_days(mut self, validity_days: i64) -> Self {
self.validity_days = Some(validity_days);
self
}
/// Set the purchase_till date directly
pub fn purchase_till(mut self, purchase_till: DateTime<Utc>) -> Self {
self.purchase_till = Some(purchase_till);
self
}
/// Set the active_till date directly
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Add a component to the product
pub fn add_component(mut self, component: ProductComponent) -> Self {
self.components.push(component);
self
}
/// Build the Product object
pub fn build(self) -> Result<Product, &'static str> {
let now = Utc::now();
let created_at = self.created_at.unwrap_or(now);
let updated_at = self.updated_at.unwrap_or(now);
// Calculate purchase_till and active_till based on validity_days if not set directly
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
let active_till = if let Some(validity_days) = self.validity_days {
self.active_till
.unwrap_or(now + Duration::days(validity_days))
} else {
self.active_till
.ok_or("Either active_till or validity_days must be provided")?
};
Ok(Product {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
price: self.price.ok_or("price is required")?,
type_: self.type_.ok_or("type_ is required")?,
category: self.category.ok_or("category is required")?,
status: self.status.ok_or("status is required")?,
created_at,
updated_at,
max_amount: self.max_amount.ok_or("max_amount is required")?,
purchase_till,
active_till,
components: self.components,
})
}
}
// Implement Storable trait
impl Storable for Product {}
// Implement Model trait
impl Model for Product {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"product"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for name
keys.push(IndexKey {
name: "name",
value: self.name.clone(),
});
// Add an index for category
keys.push(IndexKey {
name: "category",
value: self.category.clone(),
});
// Add an index for product type
keys.push(IndexKey {
name: "type",
value: format!("{:?}", self.type_),
});
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for price range
let price_range = match self.price.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "price_range",
value: price_range.to_string(),
});
// Add an index for currency code
keys.push(IndexKey {
name: "currency",
value: self.price.currency_code.clone(),
});
// Add indexes for purchasable and active products
if self.is_purchasable() {
keys.push(IndexKey {
name: "purchasable",
value: "true".to_string(),
});
}
if self.is_active() {
keys.push(IndexKey {
name: "active",
value: "true".to_string(),
});
}
keys
}
}
// Import Currency from the currency module
use crate::models::biz::Currency;

2164
herodb_old/src/models/biz/rhai/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
[package]
name = "biz_rhai"
version = "0.1.0"
edition = "2021"
[dependencies]
rhai = "1.21.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
herodb = { path = "../../../.." }
[lib]
name = "biz_rhai"
path = "src/lib.rs"
[[example]]
name = "example"
path = "examples/example.rs"

View File

@@ -0,0 +1,168 @@
// Example script demonstrating the use of Business module operations
// Custom repeat function since Rhai doesn't have a built-in repeat method
fn repeat_str(str, count) {
let result = "";
for i in 0..count {
result += str;
}
return result;
}
// Print a section header
fn print_section(title) {
let line = repeat_str("=", 50);
print(line);
print(" " + title);
print(line);
}
print_section("BUSINESS MODULE OPERATIONS EXAMPLE");
// Currency Operations
print_section("CURRENCY OPERATIONS");
// Create a currency
print("\nCreating currencies...");
let usd = create_currency(100.0, "USD");
print("USD Currency created: " + usd.amount + " " + usd.currency_code);
// Convert currencies
print("\nConverting currencies...");
let eur = create_currency(150.0, "EUR");
print("EUR Currency created: " + eur.amount + " " + eur.currency_code);
let eur_to_usd = convert_currency(eur, "USD");
print("EUR to USD: " + eur.amount + " EUR = " + eur_to_usd.amount + " USD");
// Product Component Operations
print_section("PRODUCT COMPONENT OPERATIONS");
// Create a product component
print("\nCreating product components...");
let component1 = create_product_component(1, "CPU", "Intel i7 Processor", 1);
print("Component created: " + component1.name);
print(" ID: " + component1.id);
print(" Description: " + component1.description);
print(" Quantity: " + component1.quantity);
// Create another component
let component2 = create_product_component(2, "RAM", "32GB DDR4 Memory", 2);
print("Component created: " + component2.name);
print(" ID: " + component2.id);
print(" Description: " + component2.description);
print(" Quantity: " + component2.quantity);
// Product Operations
print_section("PRODUCT OPERATIONS");
// Create a product using builder pattern
print("\nCreating a product...");
let product = create_product_builder()
.product_name("High-End Gaming PC")
.product_description("Ultimate gaming experience")
.product_price(create_currency(1999.99, "USD"))
.product_validity_days(365)
.build();
print("Product created: " + product.name);
print(" ID: " + product.id);
print(" Description: " + product.description);
print(" Price: " + product.price.amount + " " + product.price.currency_code);
// Add components to the product
print("\nAdding components to product...");
let product_with_components = product
.add_component(component1)
.add_component(component2);
print("Components added to product");
// Get components from the product
let components = get_product_components(product_with_components);
print("Number of components: " + components.len());
// Customer Operations
print_section("CUSTOMER OPERATIONS");
// Create a customer
print("\nCreating a customer...");
let customer = create_customer("John Doe", "john@example.com", "+1234567890", "123 Main St");
print("Customer created: " + customer.name);
print(" Email: " + customer.email);
print(" Phone: " + customer.phone);
print(" Address: " + customer.address);
// Sale Operations
print_section("SALE OPERATIONS");
// Create a sale
print("\nCreating a sale...");
let sale = create_sale(customer, "2025-04-19");
print("Sale created for customer: " + sale.customer.name);
print(" Date: " + sale.date);
// Add product to sale
let sale_with_item = add_sale_item(sale, product_with_components, 1);
print("Added product to sale: " + product_with_components.name);
// Service Operations
print_section("SERVICE OPERATIONS");
// Create a service
print("\nCreating a service...");
let service = create_service(
"Premium Support",
"24/7 Technical Support",
create_currency(49.99, "USD"),
30
);
print("Service created: " + service.name);
print(" Description: " + service.description);
print(" Price: " + service.price.amount + " " + service.price.currency_code);
print(" Duration: " + service.duration + " days");
// Contract Operations
print_section("CONTRACT OPERATIONS");
// Create a contract
print("\nCreating a contract...");
let contract = create_contract(
"Support Agreement",
"Annual support contract",
"2025-04-19",
"2026-04-19",
create_currency(599.99, "USD"),
"Active"
);
print("Contract created: " + contract.title);
print(" Description: " + contract.description);
print(" Start Date: " + contract.start_date);
print(" End Date: " + contract.end_date);
print(" Value: " + contract.value.amount + " " + contract.value.currency_code);
print(" Status: " + contract.status);
// Invoice Operations
print_section("INVOICE OPERATIONS");
// Create an invoice
print("\nCreating an invoice...");
let invoice = create_invoice(
"INV-2025-001",
"2025-04-19",
"2025-05-19",
customer,
create_currency(2499.99, "USD"),
"Issued",
"Pending"
);
print("Invoice created: " + invoice.number);
print(" Date: " + invoice.date);
print(" Due Date: " + invoice.due_date);
print(" Customer: " + invoice.customer.name);
print(" Amount: " + invoice.amount.amount + " " + invoice.amount.currency_code);
print(" Status: " + invoice.status);
print(" Payment Status: " + invoice.payment_status);
print_section("EXAMPLE COMPLETED");
print("All business module operations completed successfully!");

View File

@@ -0,0 +1,41 @@
use std::{fs, path::Path};
use biz_rhai::create_rhai_engine;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Business Module Rhai Wrapper Example ===");
// Create a Rhai engine with business module functionality
let mut engine = create_rhai_engine();
println!("Successfully created Rhai engine");
// Get the path to the example.rhai script
let script_path = get_script_path()?;
println!("Loading script from: {}", script_path.display());
// Load the script content
let script = fs::read_to_string(&script_path)
.map_err(|e| format!("Failed to read script file: {}", e))?;
// Run the script
println!("\n=== Running Rhai script ===");
match engine.eval::<()>(&script) {
Ok(_) => println!("\nScript executed successfully!"),
Err(e) => println!("\nScript execution error: {}", e),
}
println!("\nExample completed!");
Ok(())
}
fn get_script_path() -> Result<std::path::PathBuf, String> {
// When running with cargo run --example, the script will be in the examples directory
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("example.rhai");
if script_path.exists() {
Ok(script_path)
} else {
Err(format!("Could not find example.rhai script at {}", script_path.display()))
}
}

View File

@@ -0,0 +1,88 @@
use rhai::{Engine, EvalAltResult, Map, Dynamic, Array};
use crate::wrapper::*;
use crate::generic_wrapper::ToRhai;
/// Create a new Rhai engine with business module functionality
pub fn create_rhai_engine() -> Engine {
let mut engine = Engine::new();
// Register business module types and functions
register_business_types(&mut engine);
engine
}
/// Register business module types and functions
fn register_business_types(engine: &mut Engine) {
// Currency functions
engine.register_fn("create_currency", create_currency);
engine.register_fn("convert_currency", convert_currency);
// Product functions
engine.register_fn("create_product_builder", create_product_builder);
engine.register_fn("product_name", product_builder_name);
engine.register_fn("product_description", product_builder_description);
engine.register_fn("product_price", product_builder_price);
engine.register_fn("product_validity_days", product_builder_validity_days);
engine.register_fn("add_component", product_builder_add_component);
engine.register_fn("build", product_builder_build);
// Product component functions
engine.register_fn("create_product_component", create_product_component);
engine.register_fn("component_name", product_component_name);
engine.register_fn("component_description", product_component_description);
engine.register_fn("component_quantity", product_component_quantity);
// Sale functions
engine.register_fn("create_sale", create_sale);
engine.register_fn("add_sale_item", add_sale_item);
engine.register_fn("sale_customer", sale_customer);
engine.register_fn("sale_date", sale_date);
engine.register_fn("sale_status", sale_status);
// Customer functions
engine.register_fn("create_customer", create_customer);
engine.register_fn("customer_name", customer_name);
engine.register_fn("customer_email", customer_email);
engine.register_fn("customer_phone", customer_phone);
engine.register_fn("customer_address", customer_address);
// Service functions
engine.register_fn("create_service", create_service);
engine.register_fn("service_name", service_name);
engine.register_fn("service_description", service_description);
engine.register_fn("service_price", service_price);
engine.register_fn("service_duration", service_duration);
// Contract functions
engine.register_fn("create_contract", create_contract);
engine.register_fn("contract_title", contract_title);
engine.register_fn("contract_description", contract_description);
engine.register_fn("contract_start_date", contract_start_date);
engine.register_fn("contract_end_date", contract_end_date);
engine.register_fn("contract_value", contract_value);
engine.register_fn("contract_status", contract_status);
// Invoice functions
engine.register_fn("create_invoice", create_invoice);
engine.register_fn("invoice_number", invoice_number);
engine.register_fn("invoice_date", invoice_date);
engine.register_fn("invoice_due_date", invoice_due_date);
engine.register_fn("invoice_customer", invoice_customer);
engine.register_fn("invoice_amount", invoice_amount);
engine.register_fn("invoice_status", invoice_status);
engine.register_fn("invoice_payment_status", invoice_payment_status);
// Helper function to get components from a product
engine.register_fn("get_product_components", |product_map: Map| -> Array {
let mut array = Array::new();
if let Some(components) = product_map.get("components") {
if let Some(components_array) = components.clone().try_cast::<Array>() {
return components_array;
}
}
array
});
}

View File

@@ -0,0 +1,132 @@
use std::collections::HashMap;
use rhai::{Dynamic, Map, Array};
/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations
pub trait ToRhai {
/// Convert to a Rhai Dynamic value
fn to_rhai(&self) -> Dynamic;
}
// Implementation of ToRhai for Dynamic
impl ToRhai for Dynamic {
fn to_rhai(&self) -> Dynamic {
self.clone()
}
}
/// Generic trait for wrapping Rust functions to be used with Rhai
pub trait RhaiWrapper {
/// Wrap a function that takes ownership of self
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai;
/// Wrap a function that takes a mutable reference to self
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai;
/// Wrap a function that takes an immutable reference to self
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai;
}
/// Implementation of RhaiWrapper for any type
impl<T> RhaiWrapper for T {
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
}
/// Convert a Rhai Map to a Rust HashMap
pub fn map_to_hashmap(map: &Map) -> HashMap<String, String> {
let mut result = HashMap::new();
for (key, value) in map.iter() {
let k = key.clone().to_string();
let v = value.clone().to_string();
if !k.is_empty() && !v.is_empty() {
result.insert(k, v);
}
}
result
}
/// Convert a HashMap<String, String> to a Rhai Map
pub fn hashmap_to_map(map: &HashMap<String, String>) -> Map {
let mut result = Map::new();
for (key, value) in map.iter() {
result.insert(key.clone().into(), Dynamic::from(value.clone()));
}
result
}
/// Convert a Rhai Array to a Vec of strings
pub fn array_to_vec_string(array: &Array) -> Vec<String> {
array.iter()
.filter_map(|item| {
let s = item.clone().to_string();
if !s.is_empty() { Some(s) } else { None }
})
.collect()
}
/// Helper function to convert Dynamic to Option<String>
pub fn dynamic_to_string_option(value: &Dynamic) -> Option<String> {
if value.is_string() {
Some(value.clone().to_string())
} else {
None
}
}
/// Helper function to convert Dynamic to Option<u32>
pub fn dynamic_to_u32_option(value: &Dynamic) -> Option<u32> {
if value.is_int() {
Some(value.as_int().unwrap() as u32)
} else {
None
}
}
/// Helper function to convert Dynamic to Option<&str> with lifetime management
pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> {
if value.is_string() {
*storage = value.clone().to_string();
Some(storage.as_str())
} else {
None
}
}

View File

@@ -0,0 +1,11 @@
// Re-export the utility modules
pub mod generic_wrapper;
pub mod wrapper;
pub mod engine;
// Re-export the utility traits and functions
pub use generic_wrapper::{RhaiWrapper, map_to_hashmap, array_to_vec_string,
dynamic_to_string_option, hashmap_to_map};
pub use engine::create_rhai_engine;
// The create_rhai_engine function is now in the engine module

View File

@@ -0,0 +1,640 @@
//! Rhai wrappers for Business module functions
//!
//! This module provides Rhai wrappers for the functions in the Business module.
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map, Position};
use std::collections::HashMap;
use crate::generic_wrapper::{ToRhai, RhaiWrapper, map_to_hashmap, dynamic_to_string_option, dynamic_to_u32_option};
// Import business module types
use chrono::{DateTime, Utc, Duration};
use herodb::models::biz::{
Currency, CurrencyBuilder,
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
Customer, CustomerBuilder,
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
Service, ServiceBuilder, ServiceItem, ServiceItemBuilder, ServiceStatus, BillingFrequency,
ExchangeRate, ExchangeRateBuilder, ExchangeRateService, EXCHANGE_RATE_SERVICE,
Contract, ContractBuilder, ContractStatus,
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment
};
// Business module ToRhai implementations
// Currency ToRhai implementation
impl ToRhai for Currency {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("amount".into(), Dynamic::from(self.amount));
map.insert("currency_code".into(), Dynamic::from(self.currency_code.clone()));
Dynamic::from_map(map)
}
}
// ProductType ToRhai implementation
impl ToRhai for ProductType {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ProductType::Product => "Product",
ProductType::Service => "Service",
};
Dynamic::from(value)
}
}
// ProductStatus ToRhai implementation
impl ToRhai for ProductStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ProductStatus::Active => "Active",
ProductStatus::Error => "Error",
ProductStatus::EndOfLife => "EndOfLife",
ProductStatus::Paused => "Paused",
ProductStatus::Available => "Available",
ProductStatus::Unavailable => "Unavailable",
};
Dynamic::from(value)
}
}
// ProductComponent ToRhai implementation
impl ToRhai for ProductComponent {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
map.insert("energy_usage".into(), Dynamic::from(self.energy_usage));
map.insert("cost".into(), self.cost.to_rhai());
Dynamic::from_map(map)
}
}
// Product ToRhai implementation
impl ToRhai for Product {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("price".into(), self.price.to_rhai());
map.insert("type".into(), self.type_.to_rhai());
map.insert("category".into(), Dynamic::from(self.category.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
map.insert("max_amount".into(), Dynamic::from(self.max_amount));
map.insert("purchase_till".into(), Dynamic::from(self.purchase_till.to_string()));
map.insert("active_till".into(), Dynamic::from(self.active_till.to_string()));
// Convert components to an array
let components_array: Array = self.components.iter()
.map(|component| component.to_rhai())
.collect();
map.insert("components".into(), Dynamic::from(components_array));
Dynamic::from_map(map)
}
}
// SaleStatus ToRhai implementation
impl ToRhai for SaleStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
SaleStatus::Pending => "Pending",
SaleStatus::Completed => "Completed",
SaleStatus::Cancelled => "Cancelled",
SaleStatus::Refunded => "Refunded",
};
Dynamic::from(value)
}
}
// SaleItem ToRhai implementation
impl ToRhai for SaleItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("product_id".into(), Dynamic::from(self.product_id));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("price".into(), self.price.to_rhai());
map.insert("discount".into(), Dynamic::from(self.discount));
Dynamic::from_map(map)
}
}
// Sale ToRhai implementation
impl ToRhai for Sale {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("status".into(), self.status.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// Customer ToRhai implementation
impl ToRhai for Customer {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("email".into(), Dynamic::from(self.email.clone()));
map.insert("phone".into(), Dynamic::from(self.phone.clone()));
map.insert("address".into(), Dynamic::from(self.address.clone()));
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ServiceStatus ToRhai implementation
impl ToRhai for ServiceStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ServiceStatus::Active => "Active",
ServiceStatus::Inactive => "Inactive",
ServiceStatus::Pending => "Pending",
ServiceStatus::Cancelled => "Cancelled",
};
Dynamic::from(value)
}
}
// BillingFrequency ToRhai implementation
impl ToRhai for BillingFrequency {
fn to_rhai(&self) -> Dynamic {
let value = match self {
BillingFrequency::OneTime => "OneTime",
BillingFrequency::Daily => "Daily",
BillingFrequency::Weekly => "Weekly",
BillingFrequency::Monthly => "Monthly",
BillingFrequency::Quarterly => "Quarterly",
BillingFrequency::Yearly => "Yearly",
};
Dynamic::from(value)
}
}
// ServiceItem ToRhai implementation
impl ToRhai for ServiceItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("price".into(), self.price.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// Service ToRhai implementation
impl ToRhai for Service {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
map.insert("billing_frequency".into(), self.billing_frequency.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ContractStatus ToRhai implementation
impl ToRhai for ContractStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ContractStatus::Draft => "Draft",
ContractStatus::Pending => "Pending",
ContractStatus::Active => "Active",
ContractStatus::Completed => "Completed",
ContractStatus::Cancelled => "Cancelled",
ContractStatus::Expired => "Expired",
};
Dynamic::from(value)
}
}
// Contract ToRhai implementation
impl ToRhai for Contract {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("title".into(), Dynamic::from(self.title.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
map.insert("value".into(), self.value.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// InvoiceStatus ToRhai implementation
impl ToRhai for InvoiceStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
InvoiceStatus::Draft => "Draft",
InvoiceStatus::Sent => "Sent",
InvoiceStatus::Paid => "Paid",
InvoiceStatus::Overdue => "Overdue",
InvoiceStatus::Cancelled => "Cancelled",
};
Dynamic::from(value)
}
}
// PaymentStatus ToRhai implementation
impl ToRhai for PaymentStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
PaymentStatus::Pending => "Pending",
PaymentStatus::Completed => "Completed",
PaymentStatus::Failed => "Failed",
PaymentStatus::Refunded => "Refunded",
};
Dynamic::from(value)
}
}
// Payment ToRhai implementation
impl ToRhai for Payment {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("amount".into(), self.amount.to_rhai());
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("method".into(), Dynamic::from(self.method.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("reference".into(), Dynamic::from(self.reference.clone()));
Dynamic::from_map(map)
}
}
// InvoiceItem ToRhai implementation
impl ToRhai for InvoiceItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("unit_price".into(), self.unit_price.to_rhai());
map.insert("total".into(), self.total.to_rhai());
Dynamic::from_map(map)
}
}
// Invoice ToRhai implementation
impl ToRhai for Invoice {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("due_date".into(), Dynamic::from(self.due_date.to_string()));
map.insert("status".into(), self.status.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
// Convert payments to an array
let payments_array: Array = self.payments.iter()
.map(|payment| payment.to_rhai())
.collect();
map.insert("payments".into(), Dynamic::from(payments_array));
map.insert("subtotal".into(), self.subtotal.to_rhai());
map.insert("tax".into(), self.tax.to_rhai());
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ExchangeRate ToRhai implementation
impl ToRhai for ExchangeRate {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("from_currency".into(), Dynamic::from(self.from_currency.clone()));
map.insert("to_currency".into(), Dynamic::from(self.to_currency.clone()));
map.insert("rate".into(), Dynamic::from(self.rate));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
Dynamic::from_map(map)
}
}
//
// Business Module Function Wrappers
//
// Currency Functions
pub fn currency_new(amount: f64, currency_code: &str) -> Currency {
Currency::new(amount, currency_code.to_string())
}
pub fn currency_to_usd(currency: &Currency) -> Result<Dynamic, Box<EvalAltResult>> {
match currency.to_usd() {
Some(usd) => Ok(usd.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to convert currency to USD".into(),
Position::NONE
)))
}
}
pub fn currency_to_currency(currency: &Currency, target_currency: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match currency.to_currency(target_currency) {
Some(converted) => Ok(converted.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to convert currency to {}", target_currency).into(),
Position::NONE
)))
}
}
// CurrencyBuilder Functions
pub fn currency_builder_new() -> CurrencyBuilder {
CurrencyBuilder::new()
}
pub fn currency_builder_amount(builder: CurrencyBuilder, amount: f64) -> CurrencyBuilder {
builder.amount(amount)
}
pub fn currency_builder_currency_code(builder: CurrencyBuilder, currency_code: &str) -> CurrencyBuilder {
builder.currency_code(currency_code)
}
pub fn currency_builder_build(builder: CurrencyBuilder) -> Result<Currency, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// ProductComponent Functions
pub fn product_component_new(id: i64, name: &str, description: &str, quantity: i64) -> ProductComponent {
ProductComponent::new(id as u32, name.to_string(), description.to_string(), quantity as i32)
}
pub fn product_component_total_energy_usage(component: &ProductComponent) -> f64 {
component.total_energy_usage()
}
pub fn product_component_total_cost(component: &ProductComponent) -> Currency {
component.total_cost()
}
// ProductComponentBuilder Functions
pub fn product_component_builder_new() -> ProductComponentBuilder {
ProductComponentBuilder::new()
}
pub fn product_component_builder_id(builder: ProductComponentBuilder, id: i64) -> ProductComponentBuilder {
builder.id(id as u32)
}
pub fn product_component_builder_name(builder: ProductComponentBuilder, name: &str) -> ProductComponentBuilder {
builder.name(name)
}
pub fn product_component_builder_description(builder: ProductComponentBuilder, description: &str) -> ProductComponentBuilder {
builder.description(description)
}
pub fn product_component_builder_quantity(builder: ProductComponentBuilder, quantity: i64) -> ProductComponentBuilder {
builder.quantity(quantity as i32)
}
pub fn product_component_builder_energy_usage(builder: ProductComponentBuilder, energy_usage: f64) -> ProductComponentBuilder {
builder.energy_usage(energy_usage)
}
pub fn product_component_builder_cost(builder: ProductComponentBuilder, cost: Currency) -> ProductComponentBuilder {
builder.cost(cost)
}
pub fn product_component_builder_build(builder: ProductComponentBuilder) -> Result<ProductComponent, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// Product Functions
pub fn product_type_product() -> ProductType {
ProductType::Product
}
pub fn product_type_service() -> ProductType {
ProductType::Service
}
pub fn product_status_active() -> ProductStatus {
ProductStatus::Active
}
pub fn product_status_error() -> ProductStatus {
ProductStatus::Error
}
pub fn product_status_end_of_life() -> ProductStatus {
ProductStatus::EndOfLife
}
pub fn product_status_paused() -> ProductStatus {
ProductStatus::Paused
}
pub fn product_status_available() -> ProductStatus {
ProductStatus::Available
}
pub fn product_status_unavailable() -> ProductStatus {
ProductStatus::Unavailable
}
pub fn product_add_component(product: &mut Product, component: ProductComponent) {
product.add_component(component);
}
pub fn product_set_purchase_period(product: &mut Product, purchase_till_days: i64) {
let purchase_till = Utc::now() + Duration::days(purchase_till_days);
product.set_purchase_period(purchase_till);
}
pub fn product_set_active_period(product: &mut Product, active_till_days: i64) {
let active_till = Utc::now() + Duration::days(active_till_days);
product.set_active_period(active_till);
}
pub fn product_is_purchasable(product: &Product) -> bool {
product.is_purchasable()
}
pub fn product_is_active(product: &Product) -> bool {
product.is_active()
}
pub fn product_cost_in_currency(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match product.cost_in_currency(currency_code) {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to calculate cost in {}", currency_code).into(),
Position::NONE
)))
}
}
pub fn product_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
match product.cost_in_usd() {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to calculate cost in USD".into(),
Position::NONE
)))
}
}
pub fn product_total_energy_usage(product: &Product) -> f64 {
product.total_energy_usage()
}
pub fn product_components_cost(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match product.components_cost(currency_code) {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to calculate components cost in {}", currency_code).into(),
Position::NONE
)))
}
}
pub fn product_components_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
match product.components_cost_in_usd() {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to calculate components cost in USD".into(),
Position::NONE
)))
}
}
// ProductBuilder Functions
pub fn product_builder_new() -> ProductBuilder {
ProductBuilder::new()
}
pub fn product_builder_id(builder: ProductBuilder, id: i64) -> ProductBuilder {
builder.id(id as u32)
}
pub fn product_builder_name(builder: ProductBuilder, name: &str) -> ProductBuilder {
builder.name(name)
}
pub fn product_builder_description(builder: ProductBuilder, description: &str) -> ProductBuilder {
builder.description(description)
}
pub fn product_builder_price(builder: ProductBuilder, price: Currency) -> ProductBuilder {
builder.price(price)
}
pub fn product_builder_type(builder: ProductBuilder, type_: ProductType) -> ProductBuilder {
builder.type_(type_)
}
pub fn product_builder_category(builder: ProductBuilder, category: &str) -> ProductBuilder {
builder.category(category)
}
pub fn product_builder_status(builder: ProductBuilder, status: ProductStatus) -> ProductBuilder {
builder.status(status)
}
pub fn product_builder_max_amount(builder: ProductBuilder, max_amount: i64) -> ProductBuilder {
builder.max_amount(max_amount as u16)
}
pub fn product_builder_validity_days(builder: ProductBuilder, validity_days: i64) -> ProductBuilder {
builder.validity_days(validity_days)
}
pub fn product_builder_add_component(builder: ProductBuilder, component: ProductComponent) -> ProductBuilder {
builder.add_component(component)
}
pub fn product_builder_build(builder: ProductBuilder) -> Result<Product, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// Exchange Rate Service Functions
pub fn exchange_rate_convert(amount: f64, from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
match EXCHANGE_RATE_SERVICE.convert(amount, from_currency, to_currency) {
Some(converted) => Ok(converted),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to convert {} {} to {}", amount, from_currency, to_currency).into(),
Position::NONE
)))
}
}
pub fn exchange_rate_get_rate(from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
match EXCHANGE_RATE_SERVICE.get_rate(from_currency, to_currency) {
Some(rate) => Ok(rate),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get exchange rate from {} to {}", from_currency, to_currency).into(),
Position::NONE
)))
}
}

View File

@@ -0,0 +1,579 @@
use crate::db::{Model, Storable, DbError, DbResult, IndexKey};
use crate::models::biz::Currency; // Use crate:: for importing from the module
// use super::db::Model; // Removed old Model trait import
use chrono::{DateTime, Utc};
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Removed unused import
/// SaleStatus represents the status of a sale
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SaleStatus {
Pending,
Completed,
Cancelled,
}
/// SaleItem represents an item in a sale
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaleItem {
pub id: u32,
pub sale_id: u32,
pub product_id: Option<u32>, // ID of the product sold (if this is a product sale)
pub service_id: Option<u32>, // ID of the service sold (if this is a service sale)
pub name: String,
pub description: String, // Description of the item
pub comments: String, // Additional comments about the item
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
pub tax_amount: Currency, // Calculated tax amount
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
}
impl SaleItem {
/// Create a new sale item
pub fn new(
id: u32,
sale_id: u32,
product_id: Option<u32>,
service_id: Option<u32>,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
tax_rate: f64,
active_till: DateTime<Utc>,
) -> Self {
// Validate that either product_id or service_id is provided, but not both
assert!(
(product_id.is_some() && service_id.is_none()) ||
(product_id.is_none() && service_id.is_some()),
"Either product_id or service_id must be provided, but not both"
);
// Calculate subtotal (before tax)
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
unit_price.currency_code.clone()
);
Self {
id,
sale_id,
product_id,
service_id,
name,
description,
comments,
quantity,
unit_price,
subtotal,
tax_rate,
tax_amount,
active_till,
}
}
/// Get the total amount including tax
pub fn total_with_tax(&self) -> Currency {
Currency::new(
0, // Use 0 as a temporary ID
self.subtotal.amount + self.tax_amount.amount,
self.subtotal.currency_code.clone()
)
}
}
/// Builder for SaleItem
#[derive(Clone, CustomType)]
pub struct SaleItemBuilder {
id: Option<u32>,
sale_id: Option<u32>,
product_id: Option<u32>,
service_id: Option<u32>,
name: Option<String>,
description: Option<String>,
comments: Option<String>,
quantity: Option<i32>,
unit_price: Option<Currency>,
subtotal: Option<Currency>,
tax_rate: Option<f64>,
tax_amount: Option<Currency>,
active_till: Option<DateTime<Utc>>,
}
impl SaleItemBuilder {
/// Create a new SaleItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
sale_id: None,
product_id: None,
service_id: None,
name: None,
description: None,
comments: None,
quantity: None,
unit_price: None,
subtotal: None,
tax_rate: None,
tax_amount: None,
active_till: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self
}
/// Set the product_id
pub fn product_id(mut self, product_id: Option<u32>) -> Self {
// If setting product_id, ensure service_id is None
if product_id.is_some() {
self.service_id = None;
}
self.product_id = product_id;
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: Option<u32>) -> Self {
// If setting service_id, ensure product_id is None
if service_id.is_some() {
self.product_id = None;
}
self.service_id = service_id;
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the comments
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
self.comments = Some(comments.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the unit_price
pub fn unit_price(mut self, unit_price: Currency) -> Self {
self.unit_price = Some(unit_price);
self
}
/// Set the tax_rate
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
self.tax_rate = Some(tax_rate);
self
}
/// Set the active_till
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Build the SaleItem object
pub fn build(self) -> Result<SaleItem, &'static str> {
let unit_price = self.unit_price.ok_or("unit_price is required")?;
let quantity = self.quantity.ok_or("quantity is required")?;
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
// Validate that either product_id or service_id is provided, but not both
if self.product_id.is_none() && self.service_id.is_none() {
return Err("Either product_id or service_id must be provided");
}
if self.product_id.is_some() && self.service_id.is_some() {
return Err("Only one of product_id or service_id can be provided");
}
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
unit_price.currency_code.clone()
);
Ok(SaleItem {
id: self.id.ok_or("id is required")?,
sale_id: self.sale_id.ok_or("sale_id is required")?,
product_id: self.product_id,
service_id: self.service_id,
name: self.name.ok_or("name is required")?,
description: self.description.unwrap_or_default(),
comments: self.comments.unwrap_or_default(),
quantity,
unit_price,
subtotal,
tax_rate,
tax_amount,
active_till: self.active_till.ok_or("active_till is required")?,
})
}
}
/// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Sale {
pub id: u32,
pub customer_id: u32, // ID of the customer making the purchase
pub subtotal_amount: Currency, // Total before tax
pub tax_amount: Currency, // Total tax
pub total_amount: Currency, // Total including tax
pub status: SaleStatus,
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
pub sale_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<SaleItem>,
}
// Removed old Model trait implementation
impl Sale {
/// Create a new sale with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
status: SaleStatus,
) -> Self {
let now = Utc::now();
let zero_currency = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
Self {
id,
customer_id,
subtotal_amount: zero_currency.clone(),
tax_amount: zero_currency.clone(),
total_amount: zero_currency,
status,
service_id: None,
sale_date: now,
created_at: now,
updated_at: now,
items: Vec::new(),
}
}
/// Add an item to the sale and update the total amount
pub fn add_item(&mut self, item: SaleItem) {
// Make sure the item's sale_id matches this sale
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
// Update the amounts
if self.items.is_empty() {
// First item, initialize the amounts with the same currency
self.subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount,
item.subtotal.currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
item.tax_amount.amount,
item.tax_amount.currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount + item.tax_amount.amount,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing totals
// (Assumes all items have the same currency)
self.subtotal_amount.amount += item.subtotal.amount;
self.tax_amount.amount += item.tax_amount.amount;
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
}
// Add the item to the list
self.items.push(item);
// Update the sale timestamp
self.updated_at = Utc::now();
}
/// Recalculate all totals based on items
pub fn recalculate_totals(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].subtotal.currency_code.clone();
// Calculate the totals
let mut subtotal = 0.0;
let mut tax_total = 0.0;
for item in &self.items {
subtotal += item.subtotal.amount;
tax_total += item.tax_amount.amount;
}
// Update the amounts
self.subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
subtotal,
currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_total,
currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
subtotal + tax_total,
currency_code
);
// Update the timestamp
self.updated_at = Utc::now();
}
/// Update the status of the sale
pub fn update_status(&mut self, status: SaleStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Link this sale to an existing service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.updated_at = Utc::now();
}
}
/// Builder for Sale
#[derive(Clone, CustomType)]
pub struct SaleBuilder {
id: Option<u32>,
company_id: Option<u32>,
customer_id: Option<u32>,
buyer_name: Option<String>,
buyer_email: Option<String>,
subtotal_amount: Option<Currency>,
tax_amount: Option<Currency>,
total_amount: Option<Currency>,
status: Option<SaleStatus>,
service_id: Option<u32>,
sale_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<SaleItem>,
currency_code: Option<String>,
}
impl SaleBuilder {
/// Create a new SaleBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
company_id: None,
customer_id: None,
buyer_name: None,
buyer_email: None,
subtotal_amount: None,
tax_amount: None,
total_amount: None,
status: None,
service_id: None,
sale_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the company_id
pub fn company_id(mut self, company_id: u32) -> Self {
self.company_id = Some(company_id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the buyer_name
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
self.buyer_name = Some(buyer_name.into());
self
}
/// Set the buyer_email
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
self.buyer_email = Some(buyer_email.into());
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: SaleStatus) -> Self {
self.status = Some(status);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self
}
/// Set the sale_date
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
self.sale_date = Some(sale_date);
self
}
/// Add an item to the sale
pub fn add_item(mut self, item: SaleItem) -> Self {
self.items.push(item);
self
}
/// Build the Sale object
pub fn build(self) -> Result<Sale, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty amounts
let mut subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut tax_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate amounts from items
for item in &self.items {
// Make sure the item's sale_id matches this sale
if item.sale_id != id {
return Err("Item sale_id must match sale id");
}
subtotal_amount.amount += item.subtotal.amount;
tax_amount.amount += item.tax_amount.amount;
}
// Calculate total amount
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
Ok(Sale {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
tax_amount: self.tax_amount.unwrap_or(tax_amount),
total_amount: self.total_amount.unwrap_or(total_amount),
status: self.status.ok_or("status is required")?,
service_id: self.service_id,
sale_date: self.sale_date.unwrap_or(now),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
})
}
}
// Implement Storable trait
impl Storable for Sale {}
// Implement Model trait
impl Model for Sale {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"sale"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
keys
}
}

View File

@@ -0,0 +1,478 @@
use crate::models::biz::Currency; // Use crate:: for importing from the module
use crate::db::{Model, Storable, DbError, DbResult, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// BillingFrequency represents the frequency of billing for a service
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BillingFrequency {
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}
/// ServiceStatus represents the status of a service
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceStatus {
Active,
Paused,
Cancelled,
Completed,
}
/// ServiceItem represents an item in a service
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceItem {
pub id: u32,
pub service_id: u32,
pub product_id: u32,
pub name: String,
pub description: String, // Description of the service item
pub comments: String, // Additional comments about the service item
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
pub active_till: DateTime<Utc>,
}
impl ServiceItem {
/// Create a new service item
pub fn new(
id: u32,
service_id: u32,
product_id: u32,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
active_till: DateTime<Utc>,
) -> Self {
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
Self {
id,
service_id,
product_id,
name,
description,
comments,
quantity,
unit_price,
subtotal,
active_till,
}
}
/// Calculate the subtotal based on quantity and unit price
pub fn calculate_subtotal(&mut self) {
let amount = self.unit_price.amount * self.quantity as f64;
self.subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
self.unit_price.currency_code.clone()
);
}
}
/// Builder for ServiceItem
pub struct ServiceItemBuilder {
id: Option<u32>,
service_id: Option<u32>,
product_id: Option<u32>,
name: Option<String>,
description: Option<String>,
comments: Option<String>,
quantity: Option<i32>,
unit_price: Option<Currency>,
subtotal: Option<Currency>,
tax_rate: Option<f64>,
tax_amount: Option<Currency>,
is_taxable: Option<bool>,
active_till: Option<DateTime<Utc>>,
}
impl ServiceItemBuilder {
/// Create a new ServiceItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
service_id: None,
product_id: None,
name: None,
description: None,
comments: None,
quantity: None,
unit_price: None,
subtotal: None,
tax_rate: None,
tax_amount: None,
is_taxable: None,
active_till: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self
}
/// Set the product_id
pub fn product_id(mut self, product_id: u32) -> Self {
self.product_id = Some(product_id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the comments
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
self.comments = Some(comments.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the unit_price
pub fn unit_price(mut self, unit_price: Currency) -> Self {
self.unit_price = Some(unit_price);
self
}
/// Set the tax_rate
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
self.tax_rate = Some(tax_rate);
self
}
/// Set is_taxable
pub fn is_taxable(mut self, is_taxable: bool) -> Self {
self.is_taxable = Some(is_taxable);
self
}
/// Set the active_till
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Build the ServiceItem object
pub fn build(self) -> Result<ServiceItem, &'static str> {
let unit_price = self.unit_price.ok_or("unit_price is required")?;
let quantity = self.quantity.ok_or("quantity is required")?;
let tax_rate = self.tax_rate.unwrap_or(0.0);
let is_taxable = self.is_taxable.unwrap_or(false);
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount if taxable
let tax_amount = if is_taxable {
Currency::new(
0, // Use 0 as a temporary ID
subtotal.amount * tax_rate,
unit_price.currency_code.clone()
)
} else {
Currency::new(
0, // Use 0 as a temporary ID
0.0,
unit_price.currency_code.clone()
)
};
Ok(ServiceItem {
id: self.id.ok_or("id is required")?,
service_id: self.service_id.ok_or("service_id is required")?,
product_id: self.product_id.ok_or("product_id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.unwrap_or_default(),
comments: self.comments.unwrap_or_default(),
quantity,
unit_price,
subtotal,
active_till: self.active_till.ok_or("active_till is required")?,
})
}
}
/// Service represents a recurring service with billing frequency
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
pub id: u32,
pub customer_id: u32,
pub total_amount: Currency,
pub status: ServiceStatus,
pub billing_frequency: BillingFrequency,
pub service_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<ServiceItem>,
}
impl Service {
/// Create a new service with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
status: ServiceStatus,
billing_frequency: BillingFrequency,
) -> Self {
let now = Utc::now();
Self {
id,
customer_id,
total_amount: Currency::new(0, 0.0, currency_code),
status,
billing_frequency,
service_date: now,
created_at: now,
updated_at: now,
items: Vec::new(),
}
}
/// Add an item to the service and update the total amount
pub fn add_item(&mut self, item: ServiceItem) {
// Make sure the item's service_id matches this service
assert_eq!(self.id, item.service_id, "Item service_id must match service id");
// Update the total amount
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount ,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
self.total_amount.amount += item.subtotal.amount;
}
// Add the item to the list
self.items.push(item);
// Update the service timestamp
self.updated_at = Utc::now();
}
/// Calculate the total amount based on all items
pub fn calculate_total(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].subtotal.currency_code.clone();
// Calculate the total amount
let mut total = 0.0;
for item in &self.items {
total += item.subtotal.amount;
}
// Update the total amount
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
total,
currency_code
);
// Update the service timestamp
self.updated_at = Utc::now();
}
/// Update the status of the service
pub fn update_status(&mut self, status: ServiceStatus) {
self.status = status;
self.updated_at = Utc::now();
}
}
/// Builder for Service
pub struct ServiceBuilder {
id: Option<u32>,
customer_id: Option<u32>,
total_amount: Option<Currency>,
status: Option<ServiceStatus>,
billing_frequency: Option<BillingFrequency>,
service_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<ServiceItem>,
currency_code: Option<String>,
}
impl ServiceBuilder {
/// Create a new ServiceBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
total_amount: None,
status: None,
billing_frequency: None,
service_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: ServiceStatus) -> Self {
self.status = Some(status);
self
}
/// Set the billing_frequency
pub fn billing_frequency(mut self, billing_frequency: BillingFrequency) -> Self {
self.billing_frequency = Some(billing_frequency);
self
}
/// Set the service_date
pub fn service_date(mut self, service_date: DateTime<Utc>) -> Self {
self.service_date = Some(service_date);
self
}
/// Add an item to the service
pub fn add_item(mut self, item: ServiceItem) -> Self {
self.items.push(item);
self
}
/// Build the Service object
pub fn build(self) -> Result<Service, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty total amount
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate total amount from items
for item in &self.items {
// Make sure the item's service_id matches this service
if item.service_id != id {
return Err("Item service_id must match service id");
}
if total_amount.amount == 0.0 {
// First item, initialize the total amount with the same currency
total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
total_amount.amount += item.subtotal.amount ;
}
}
Ok(Service {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
total_amount: self.total_amount.unwrap_or(total_amount),
status: self.status.ok_or("status is required")?,
billing_frequency: self.billing_frequency.ok_or("billing_frequency is required")?,
service_date: self.service_date.unwrap_or(now),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
})
}
}
// Implement Storable trait
impl Storable for Service {
}
// Implement Model trait
impl Model for Service {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"service"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
keys
}
}