...
This commit is contained in:
530
herodb_old/src/models/biz/README.md
Normal file
530
herodb_old/src/models/biz/README.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# Business Models
|
||||
|
||||
This directory contains the core business models used throughout the application for representing essential business objects like products, sales, and currency.
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Customer │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ │ │ │
|
||||
└─────────────┘ └─────────────┘ │ │ │ │
|
||||
▲ │ SaleItem │◄────┤ Sale │
|
||||
│ │ │ │ │
|
||||
┌─────┴──────────┐ │ │ │ │
|
||||
│ProductComponent│ └─────────────┘ └──────┬──────┘
|
||||
└────────────────┘ ▲ │
|
||||
/ │
|
||||
┌─────────────┐ ┌─────────────┐ / │
|
||||
│ Currency │◄────┤ Service │◄────────/ │
|
||||
└─────────────┘ └─────────────┘ │
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ InvoiceItem │◄────┤ Invoice │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Business Logic Relationships
|
||||
|
||||
- **Customer**: The entity purchasing products or services
|
||||
- **Product/Service**: Defines what is being sold, including its base price
|
||||
- Can be marked as a template (`is_template=true`) to create copies for actual sales
|
||||
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
|
||||
- Contains SaleItems that can be linked to either Products or Services
|
||||
- **SaleItem**: Represents an item within a sale
|
||||
- Can be linked to either a Product or a Service (via product_id or service_id)
|
||||
- **Service**: Represents an ongoing service provided to a customer
|
||||
- Created from a Product template when the product type is Service
|
||||
- **Invoice**: Represents the billing document for a sale, with payment tracking
|
||||
- Created from a Sale object to handle billing and payment tracking
|
||||
|
||||
## Root Objects
|
||||
|
||||
- Root objects are the ones stored directly in the DB
|
||||
- Root Objects are:
|
||||
- Customer
|
||||
- Currency
|
||||
- Product
|
||||
- Sale
|
||||
- Service
|
||||
- Invoice
|
||||
|
||||
## Models
|
||||
|
||||
### Currency (Root Object)
|
||||
|
||||
Represents a monetary value with an amount and currency code.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: f64 - The monetary amount
|
||||
- `currency_code`: String - The currency code (e.g., "USD", "EUR")
|
||||
|
||||
**Builder:**
|
||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
||||
|
||||
### Customer (Root Object)
|
||||
|
||||
Represents a customer who can purchase products or services.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Customer name
|
||||
- `description`: String - Customer description
|
||||
- `pubkey`: String - Customer's public key
|
||||
- `contact_ids`: Vec<u32> - List of contact IDs
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Methods:**
|
||||
- `add_contact()` - Adds a contact ID to the customer
|
||||
- `remove_contact()` - Removes a contact ID from the customer
|
||||
|
||||
### Product
|
||||
|
||||
#### ProductType Enum
|
||||
Categorizes products:
|
||||
- `Product` - Physical product
|
||||
- `Service` - Non-physical service
|
||||
|
||||
#### ProductStatus Enum
|
||||
Tracks product availability:
|
||||
- `Available` - Product can be purchased
|
||||
- `Unavailable` - Product cannot be purchased
|
||||
|
||||
#### ProductComponent
|
||||
Represents a component part of a product.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Component name
|
||||
- `description`: String - Component description
|
||||
- `quantity`: i32 - Number of this component
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Builder:**
|
||||
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
|
||||
|
||||
#### Product (Root Object)
|
||||
Represents a product or service offered.
|
||||
|
||||
**Properties:**
|
||||
- `id`: i64 - Unique identifier
|
||||
- `name`: String - Product name
|
||||
- `description`: String - Product description
|
||||
- `price`: Currency - Product price
|
||||
- `type_`: ProductType - Product or Service
|
||||
- `category`: String - Product category
|
||||
- `status`: ProductStatus - Available or Unavailable
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `max_amount`: i64 - Maximum quantity available
|
||||
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||
- `active_till`: DateTime<Utc> - When product/service expires
|
||||
- `components`: Vec<ProductComponent> - List of product components
|
||||
- `is_template`: bool - Whether this is a template product (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_component()` - Adds a component to this product
|
||||
- `set_purchase_period()` - Updates purchase availability timeframe
|
||||
- `set_active_period()` - Updates active timeframe
|
||||
- `is_purchasable()` - Checks if product is available for purchase
|
||||
- `is_active()` - Checks if product is still active
|
||||
|
||||
**Builder:**
|
||||
- `ProductBuilder` - Provides a fluent interface for creating Product instances
|
||||
|
||||
**Database Implementation:**
|
||||
- Implements `Storable` trait for serialization
|
||||
- Implements `SledModel` trait with:
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `db_prefix()` - Returns "product" as the database prefix
|
||||
|
||||
### Service (Root Object)
|
||||
|
||||
#### BillingFrequency Enum
|
||||
Defines how often a service is billed:
|
||||
- `Hourly` - Billed by the hour
|
||||
- `Daily` - Billed daily
|
||||
- `Weekly` - Billed weekly
|
||||
- `Monthly` - Billed monthly
|
||||
- `Yearly` - Billed yearly
|
||||
|
||||
#### ServiceStatus Enum
|
||||
Tracks the status of a service:
|
||||
- `Active` - Service is currently active
|
||||
- `Paused` - Service is temporarily paused
|
||||
- `Cancelled` - Service has been cancelled
|
||||
- `Completed` - Service has been completed
|
||||
|
||||
#### ServiceItem
|
||||
Represents an item within a service.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `service_id`: u32 - Parent service ID
|
||||
- `product_id`: u32 - ID of the product this service is based on
|
||||
- `name`: String - Service name
|
||||
- `description`: String - Detailed description of the service item
|
||||
- `comments`: String - Additional notes or comments about the service item
|
||||
- `quantity`: i32 - Number of units
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price before tax
|
||||
- `tax_rate`: f64 - Tax rate as a percentage
|
||||
- `tax_amount`: Currency - Calculated tax amount
|
||||
- `is_taxable`: bool - Whether this item is taxable
|
||||
- `active_till`: DateTime<Utc> - When service expires
|
||||
|
||||
#### Service
|
||||
Represents an ongoing service provided to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer receiving the service
|
||||
- `total_amount`: Currency - Total service amount including tax
|
||||
- `status`: ServiceStatus - Current service status
|
||||
- `billing_frequency`: BillingFrequency - How often the service is billed
|
||||
- `service_date`: DateTime<Utc> - When service started
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<ServiceItem> - List of items in the service
|
||||
- `is_template`: bool - Whether this is a template service (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the service and updates total
|
||||
- `calculate_total()` - Recalculates the total amount
|
||||
- `update_status()` - Updates the status of the service
|
||||
|
||||
### Sale
|
||||
|
||||
#### SaleStatus Enum
|
||||
Tracks the status of a sale:
|
||||
- `Pending` - Sale is in progress
|
||||
- `Completed` - Sale has been finalized
|
||||
- `Cancelled` - Sale has been cancelled
|
||||
|
||||
#### SaleItem
|
||||
Represents an item within a sale.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `sale_id`: u32 - Parent sale ID
|
||||
- `product_id`: Option<u32> - ID of the product sold (if this is a product sale)
|
||||
- `service_id`: Option<u32> - ID of the service sold (if this is a service sale)
|
||||
- `name`: String - Product/service name at time of sale
|
||||
- `description`: String - Detailed description of the item
|
||||
- `comments`: String - Additional notes or comments about the item
|
||||
- `quantity`: i32 - Number of items purchased
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price for this item before tax (calculated)
|
||||
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
- `tax_amount`: Currency - Calculated tax amount for this item
|
||||
- `active_till`: DateTime<Utc> - When item/service expires
|
||||
|
||||
**Methods:**
|
||||
- `total_with_tax()` - Returns the total amount including tax
|
||||
|
||||
**Builder:**
|
||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||
|
||||
#### Sale (Root Object)
|
||||
Represents a complete sale transaction.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `company_id`: u32 - ID of the company making the sale
|
||||
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
|
||||
- `buyer_name`: String - Name of the buyer
|
||||
- `buyer_email`: String - Email of the buyer
|
||||
- `subtotal_amount`: Currency - Total sale amount before tax
|
||||
- `tax_amount`: Currency - Total tax amount for the sale
|
||||
- `total_amount`: Currency - Total sale amount including tax
|
||||
- `status`: SaleStatus - Current sale status
|
||||
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
|
||||
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<SaleItem> - List of items in the sale
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the sale and updates totals
|
||||
- `update_status()` - Updates the status of the sale
|
||||
- `recalculate_totals()` - Recalculates all totals based on items
|
||||
- `create_service()` - Creates a service from this sale (to be added)
|
||||
|
||||
**Builder:**
|
||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||
|
||||
**Database Implementation:**
|
||||
- Implements `Storable` trait for serialization
|
||||
- Implements `SledModel` trait with:
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `db_prefix()` - Returns "sale" as the database prefix
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a Currency
|
||||
|
||||
```rust
|
||||
let price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
```
|
||||
|
||||
### Creating a Product
|
||||
|
||||
```rust
|
||||
// Create a currency using the builder
|
||||
let price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
|
||||
// Create a component using the builder
|
||||
let component = ProductComponentBuilder::new()
|
||||
.id(1)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.build()
|
||||
.expect("Failed to build product component");
|
||||
|
||||
// Create a product using the builder
|
||||
let product = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Premium Service")
|
||||
.description("Our premium service offering")
|
||||
.price(price)
|
||||
.type_(ProductType::Service)
|
||||
.category("Services")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(30)
|
||||
.add_component(component)
|
||||
.build()
|
||||
.expect("Failed to build product");
|
||||
```
|
||||
|
||||
### Creating a Sale
|
||||
|
||||
```rust
|
||||
let now = Utc::now();
|
||||
|
||||
// Create a currency using the builder
|
||||
let unit_price = CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()
|
||||
.expect("Failed to build currency");
|
||||
|
||||
// Create a sale item using the builder
|
||||
let item = SaleItemBuilder::new()
|
||||
.id(1)
|
||||
.sale_id(1)
|
||||
.product_id(1)
|
||||
.name("Premium Service")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0) // 20% tax rate
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
|
||||
// Create a sale using the builder
|
||||
let mut sale = SaleBuilder::new()
|
||||
.id(1)
|
||||
.company_id(101)
|
||||
.buyer_name("John Doe")
|
||||
.buyer_email("john.doe@example.com")
|
||||
.currency_code("USD")
|
||||
.status(SaleStatus::Pending)
|
||||
.add_item(item)
|
||||
.build()
|
||||
.expect("Failed to build sale");
|
||||
|
||||
// Update the sale status
|
||||
sale.update_status(SaleStatus::Completed);
|
||||
|
||||
// The sale now contains:
|
||||
// - subtotal_amount: The sum of all items before tax
|
||||
// - tax_amount: The sum of all tax amounts
|
||||
// - total_amount: The total including tax
|
||||
```
|
||||
|
||||
### Relationship Between Sale and Invoice
|
||||
|
||||
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
|
||||
|
||||
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
|
||||
|
||||
```rust
|
||||
// Create an invoice item linked to a sale
|
||||
let invoice_item = InvoiceItemBuilder::new()
|
||||
.id(1)
|
||||
.invoice_id(1)
|
||||
.description("Premium Service")
|
||||
.amount(sale.total_amount.clone()) // Use the total amount from the sale
|
||||
.sale_id(sale.id) // Link to the sale
|
||||
.build()
|
||||
.expect("Failed to build invoice item");
|
||||
```
|
||||
|
||||
## Database Operations
|
||||
|
||||
The library provides model-specific convenience methods for common database operations:
|
||||
|
||||
```rust
|
||||
// Insert a product
|
||||
db.insert_product(&product).expect("Failed to insert product");
|
||||
|
||||
// Retrieve a product by ID
|
||||
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
|
||||
|
||||
// List all products
|
||||
let all_products = db.list_products().expect("Failed to list products");
|
||||
|
||||
// Delete a product
|
||||
db.delete_product(1).expect("Failed to delete product");
|
||||
```
|
||||
|
||||
These methods are available for all root objects:
|
||||
|
||||
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
||||
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
|
||||
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
|
||||
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
|
||||
|
||||
### Invoice (Root Object)
|
||||
|
||||
#### InvoiceStatus Enum
|
||||
Tracks the status of an invoice:
|
||||
- `Draft` - Invoice is in draft state
|
||||
- `Sent` - Invoice has been sent to the customer
|
||||
- `Paid` - Invoice has been paid
|
||||
- `Overdue` - Invoice is past due date
|
||||
- `Cancelled` - Invoice has been cancelled
|
||||
|
||||
#### PaymentStatus Enum
|
||||
Tracks the payment status of an invoice:
|
||||
- `Unpaid` - Invoice has not been paid
|
||||
- `PartiallyPaid` - Invoice has been partially paid
|
||||
- `Paid` - Invoice has been fully paid
|
||||
|
||||
#### Payment
|
||||
Represents a payment made against an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: Currency - Payment amount
|
||||
- `date`: DateTime<Utc> - Payment date
|
||||
- `method`: String - Payment method
|
||||
- `comment`: String - Payment comment
|
||||
|
||||
#### InvoiceItem
|
||||
Represents an item in an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `invoice_id`: u32 - Parent invoice ID
|
||||
- `description`: String - Item description
|
||||
- `amount`: Currency - Item amount
|
||||
- `service_id`: Option<u32> - ID of the service this item is for
|
||||
- `sale_id`: Option<u32> - ID of the sale this item is for
|
||||
|
||||
**Methods:**
|
||||
- `link_to_service()` - Links the invoice item to a service
|
||||
- `link_to_sale()` - Links the invoice item to a sale
|
||||
|
||||
#### Invoice
|
||||
Represents an invoice sent to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer being invoiced
|
||||
- `total_amount`: Currency - Total invoice amount
|
||||
- `balance_due`: Currency - Amount still due
|
||||
- `status`: InvoiceStatus - Current invoice status
|
||||
- `payment_status`: PaymentStatus - Current payment status
|
||||
- `issue_date`: DateTime<Utc> - When invoice was issued
|
||||
- `due_date`: DateTime<Utc> - When payment is due
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<InvoiceItem> - List of items in the invoice
|
||||
- `payments`: Vec<Payment> - List of payments made
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the invoice
|
||||
- `calculate_total()` - Calculates the total amount
|
||||
- `add_payment()` - Adds a payment to the invoice
|
||||
- `calculate_balance()` - Calculates the balance due
|
||||
- `update_payment_status()` - Updates the payment status
|
||||
- `update_status()` - Updates the status of the invoice
|
||||
- `is_overdue()` - Checks if the invoice is overdue
|
||||
- `check_if_overdue()` - Marks the invoice as overdue if past due date
|
||||
|
||||
### Relationships Between Models
|
||||
|
||||
#### Product/Service Templates and Instances
|
||||
|
||||
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
|
||||
|
||||
#### Sale to Service Relationship
|
||||
|
||||
A SaleItem can be directly linked to a Service via the `service_id` field. This allows for selling existing services or creating new services as part of a sale:
|
||||
|
||||
```rust
|
||||
// Create a SaleItem linked to a service
|
||||
let sale_item = SaleItemBuilder::new()
|
||||
.id(1)
|
||||
.sale_id(1)
|
||||
.service_id(Some(42)) // Link to service with ID 42
|
||||
.product_id(None) // No product link since this is a service
|
||||
.name("Premium Support")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
```
|
||||
|
||||
#### Sale to Invoice Relationship
|
||||
|
||||
An Invoice is created from a Sale to handle billing and payment tracking:
|
||||
|
||||
```rust
|
||||
// Create an invoice from a sale
|
||||
let invoice = Invoice::from_sale(
|
||||
invoice_id,
|
||||
sale,
|
||||
due_date
|
||||
);
|
||||
```
|
||||
|
||||
#### Customer-Centric View
|
||||
|
||||
The models allow tracking all customer interactions:
|
||||
|
||||
- What products/services they've purchased (via Sale records)
|
||||
- What ongoing services they have (via Service records)
|
||||
- What they've been invoiced for (via Invoice records)
|
||||
- What they've paid (via Payment records in Invoices)
|
||||
|
||||
```rust
|
||||
// Get all sales for a customer
|
||||
let customer_sales = db.list_sales_by_customer(customer_id);
|
||||
|
||||
// Get all services for a customer
|
||||
let customer_services = db.list_services_by_customer(customer_id);
|
||||
|
||||
// Get all invoices for a customer
|
||||
let customer_invoices = db.list_invoices_by_customer(customer_id);
|
||||
```
|
293
herodb_old/src/models/biz/contract.rs
Normal file
293
herodb_old/src/models/biz/contract.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ContractStatus represents the status of a contract
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ContractStatus {
|
||||
Active,
|
||||
Expired,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
/// Contract represents a legal agreement between a customer and the business
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Contract {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub service_id: Option<u32>,
|
||||
pub sale_id: Option<u32>,
|
||||
pub terms: String,
|
||||
pub start_date: DateTime<Utc>,
|
||||
pub end_date: DateTime<Utc>,
|
||||
pub auto_renewal: bool,
|
||||
pub renewal_terms: String,
|
||||
pub status: ContractStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Contract {
|
||||
/// Create a new contract with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
terms: String,
|
||||
start_date: DateTime<Utc>,
|
||||
end_date: DateTime<Utc>,
|
||||
auto_renewal: bool,
|
||||
renewal_terms: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
terms,
|
||||
start_date,
|
||||
end_date,
|
||||
auto_renewal,
|
||||
renewal_terms,
|
||||
status: ContractStatus::Active,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Link the contract to a service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // A contract can only be linked to either a service or a sale
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link the contract to a sale
|
||||
pub fn link_to_sale(&mut self, sale_id: u32) {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // A contract can only be linked to either a service or a sale
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Check if the contract is currently active
|
||||
pub fn is_active(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
self.status == ContractStatus::Active &&
|
||||
now >= self.start_date &&
|
||||
now <= self.end_date
|
||||
}
|
||||
|
||||
/// Check if the contract has expired
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
now > self.end_date
|
||||
}
|
||||
|
||||
/// Update the contract status
|
||||
pub fn update_status(&mut self, status: ContractStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Renew the contract based on renewal terms
|
||||
pub fn renew(&mut self) -> Result<(), &'static str> {
|
||||
if !self.auto_renewal {
|
||||
return Err("Contract is not set for auto-renewal");
|
||||
}
|
||||
|
||||
if self.status != ContractStatus::Active {
|
||||
return Err("Cannot renew a non-active contract");
|
||||
}
|
||||
|
||||
// Calculate new dates based on the current end date
|
||||
let duration = self.end_date - self.start_date;
|
||||
self.start_date = self.end_date;
|
||||
self.end_date = self.end_date + duration;
|
||||
|
||||
self.updated_at = Utc::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Contract
|
||||
pub struct ContractBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
terms: Option<String>,
|
||||
start_date: Option<DateTime<Utc>>,
|
||||
end_date: Option<DateTime<Utc>>,
|
||||
auto_renewal: Option<bool>,
|
||||
renewal_terms: Option<String>,
|
||||
status: Option<ContractStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ContractBuilder {
|
||||
/// Create a new ContractBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
terms: None,
|
||||
start_date: None,
|
||||
end_date: None,
|
||||
auto_renewal: None,
|
||||
renewal_terms: None,
|
||||
status: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // A contract can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // A contract can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the terms
|
||||
pub fn terms<S: Into<String>>(mut self, terms: S) -> Self {
|
||||
self.terms = Some(terms.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the start_date
|
||||
pub fn start_date(mut self, start_date: DateTime<Utc>) -> Self {
|
||||
self.start_date = Some(start_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the end_date
|
||||
pub fn end_date(mut self, end_date: DateTime<Utc>) -> Self {
|
||||
self.end_date = Some(end_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set auto_renewal
|
||||
pub fn auto_renewal(mut self, auto_renewal: bool) -> Self {
|
||||
self.auto_renewal = Some(auto_renewal);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the renewal_terms
|
||||
pub fn renewal_terms<S: Into<String>>(mut self, renewal_terms: S) -> Self {
|
||||
self.renewal_terms = Some(renewal_terms.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ContractStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Contract object
|
||||
pub fn build(self) -> Result<Contract, &'static str> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Validate that start_date is before end_date
|
||||
let start_date = self.start_date.ok_or("start_date is required")?;
|
||||
let end_date = self.end_date.ok_or("end_date is required")?;
|
||||
|
||||
if start_date >= end_date {
|
||||
return Err("start_date must be before end_date");
|
||||
}
|
||||
|
||||
Ok(Contract {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_id: self.sale_id,
|
||||
terms: self.terms.ok_or("terms is required")?,
|
||||
start_date,
|
||||
end_date,
|
||||
auto_renewal: self.auto_renewal.unwrap_or(false),
|
||||
renewal_terms: self.renewal_terms.ok_or("renewal_terms is required")?,
|
||||
status: self.status.unwrap_or(ContractStatus::Active),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Storable for Contract {}
|
||||
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Contract {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"contract"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for service_id if present
|
||||
if let Some(service_id) = self.service_id {
|
||||
keys.push(IndexKey {
|
||||
name: "service_id",
|
||||
value: service_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add an index for sale_id if present
|
||||
if let Some(sale_id) = self.sale_id {
|
||||
keys.push(IndexKey {
|
||||
name: "sale_id",
|
||||
value: sale_id.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for active contracts
|
||||
if self.is_active() {
|
||||
keys.push(IndexKey {
|
||||
name: "active",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
114
herodb_old/src/models/biz/currency.rs
Normal file
114
herodb_old/src/models/biz/currency.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use crate::db::model::{Model, IndexKey};
|
||||
use crate::db::{Storable, DbError, DbResult};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Currency represents a monetary value with amount and currency code
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Currency {
|
||||
pub id: u32,
|
||||
pub amount: f64,
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
impl Currency {
|
||||
/// Create a new currency with amount and code
|
||||
pub fn new(id: u32, amount: f64, currency_code: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
amount,
|
||||
currency_code,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn amount(&mut self) -> f64 {
|
||||
self.amount
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Currency
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct CurrencyBuilder {
|
||||
id: Option<u32>,
|
||||
amount: Option<f64>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl CurrencyBuilder {
|
||||
/// Create a new CurrencyBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
amount: None,
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount
|
||||
pub fn amount(mut self, amount: f64) -> Self {
|
||||
self.amount = Some(amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Currency object
|
||||
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
|
||||
Ok(Currency {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Currency {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Currency {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"currency"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for currency_code
|
||||
keys.push(IndexKey {
|
||||
name: "currency_code",
|
||||
value: self.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add an index for amount range
|
||||
// This allows finding currencies within specific ranges
|
||||
let amount_range = match self.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "amount_range",
|
||||
value: amount_range.to_string(),
|
||||
});
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
166
herodb_old/src/models/biz/customer.rs
Normal file
166
herodb_old/src/models/biz/customer.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Customer represents a customer who can purchase products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Customer {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub pubkey: String,
|
||||
pub contact_ids: Vec<u32>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Customer {
|
||||
/// Create a new customer with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
pubkey: String,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
pubkey,
|
||||
contact_ids: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a contact ID to the customer
|
||||
pub fn add_contact(&mut self, contact_id: u32) {
|
||||
if !self.contact_ids.contains(&contact_id) {
|
||||
self.contact_ids.push(contact_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a contact ID from the customer
|
||||
pub fn remove_contact(&mut self, contact_id: u32) -> bool {
|
||||
let len = self.contact_ids.len();
|
||||
self.contact_ids.retain(|&id| id != contact_id);
|
||||
|
||||
if self.contact_ids.len() < len {
|
||||
self.updated_at = Utc::now();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Customer
|
||||
pub struct CustomerBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
pubkey: Option<String>,
|
||||
contact_ids: Vec<u32>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl CustomerBuilder {
|
||||
/// Create a new CustomerBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
pubkey: None,
|
||||
contact_ids: Vec::new(),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the pubkey
|
||||
pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
|
||||
self.pubkey = Some(pubkey.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a contact ID
|
||||
pub fn add_contact(mut self, contact_id: u32) -> Self {
|
||||
self.contact_ids.push(contact_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set multiple contact IDs
|
||||
pub fn contact_ids(mut self, contact_ids: Vec<u32>) -> Self {
|
||||
self.contact_ids = contact_ids;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Customer object
|
||||
pub fn build(self) -> Result<Customer, &'static str> {
|
||||
let now = Utc::now();
|
||||
|
||||
Ok(Customer {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
pubkey: self.pubkey.ok_or("pubkey is required")?,
|
||||
contact_ids: self.contact_ids,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Customer {}
|
||||
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Customer {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"customer"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for the name
|
||||
keys.push(IndexKey {
|
||||
name: "name",
|
||||
value: self.name.clone(),
|
||||
});
|
||||
|
||||
// Add an index for the pubkey
|
||||
keys.push(IndexKey {
|
||||
name: "pubkey",
|
||||
value: self.pubkey.clone(),
|
||||
});
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
178
herodb_old/src/models/biz/exchange_rate.rs
Normal file
178
herodb_old/src/models/biz/exchange_rate.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::model::{Model, Storable};
|
||||
|
||||
/// ExchangeRate represents an exchange rate between two currencies
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExchangeRate {
|
||||
pub id: u32,
|
||||
pub base_currency: String,
|
||||
pub target_currency: String,
|
||||
pub rate: f64,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ExchangeRate {
|
||||
/// Create a new exchange rate
|
||||
pub fn new(id: u32, base_currency: String, target_currency: String, rate: f64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
base_currency,
|
||||
target_currency,
|
||||
rate,
|
||||
timestamp: Utc::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for ExchangeRate
|
||||
pub struct ExchangeRateBuilder {
|
||||
id: Option<u32>,
|
||||
base_currency: Option<String>,
|
||||
target_currency: Option<String>,
|
||||
rate: Option<f64>,
|
||||
timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ExchangeRateBuilder {
|
||||
/// Create a new ExchangeRateBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
base_currency: None,
|
||||
target_currency: None,
|
||||
rate: None,
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the base currency
|
||||
pub fn base_currency<S: Into<String>>(mut self, base_currency: S) -> Self {
|
||||
self.base_currency = Some(base_currency.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the target currency
|
||||
pub fn target_currency<S: Into<String>>(mut self, target_currency: S) -> Self {
|
||||
self.target_currency = Some(target_currency.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the rate
|
||||
pub fn rate(mut self, rate: f64) -> Self {
|
||||
self.rate = Some(rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timestamp
|
||||
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
|
||||
self.timestamp = Some(timestamp);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ExchangeRate object
|
||||
pub fn build(self) -> Result<ExchangeRate, &'static str> {
|
||||
let now = Utc::now();
|
||||
Ok(ExchangeRate {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
base_currency: self.base_currency.ok_or("base_currency is required")?,
|
||||
target_currency: self.target_currency.ok_or("target_currency is required")?,
|
||||
rate: self.rate.ok_or("rate is required")?,
|
||||
timestamp: self.timestamp.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Storable for ExchangeRate {}
|
||||
// Implement Model trait
|
||||
impl Model for ExchangeRate {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"exchange_rate"
|
||||
}
|
||||
}
|
||||
|
||||
/// ExchangeRateService provides methods to get and set exchange rates
|
||||
#[derive(Clone)]
|
||||
pub struct ExchangeRateService {
|
||||
rates: Arc<Mutex<HashMap<String, ExchangeRate>>>,
|
||||
}
|
||||
|
||||
impl ExchangeRateService {
|
||||
/// Create a new exchange rate service
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rates: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an exchange rate
|
||||
pub fn set_rate(&self, exchange_rate: ExchangeRate) {
|
||||
let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency);
|
||||
let mut rates = self.rates.lock().unwrap();
|
||||
rates.insert(key, exchange_rate);
|
||||
}
|
||||
|
||||
/// Get an exchange rate
|
||||
pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option<ExchangeRate> {
|
||||
let key = format!("{}_{}", base_currency, target_currency);
|
||||
let rates = self.rates.lock().unwrap();
|
||||
rates.get(&key).cloned()
|
||||
}
|
||||
|
||||
/// Convert an amount from one currency to another
|
||||
pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option<f64> {
|
||||
// If the currencies are the same, return the amount
|
||||
if from_currency == to_currency {
|
||||
return Some(amount);
|
||||
}
|
||||
|
||||
// Try to get the direct exchange rate
|
||||
if let Some(rate) = self.get_rate(from_currency, to_currency) {
|
||||
return Some(amount * rate.rate);
|
||||
}
|
||||
|
||||
// Try to get the inverse exchange rate
|
||||
if let Some(rate) = self.get_rate(to_currency, from_currency) {
|
||||
return Some(amount / rate.rate);
|
||||
}
|
||||
|
||||
// Try to convert via USD
|
||||
if from_currency != "USD" && to_currency != "USD" {
|
||||
if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") {
|
||||
return self.convert(from_to_usd, "USD", to_currency);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Create a global instance of the exchange rate service
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = {
|
||||
let service = ExchangeRateService::new();
|
||||
|
||||
// Set some default exchange rates
|
||||
service.set_rate(ExchangeRate::new(1, "USD".to_string(), "EUR".to_string(), 0.85));
|
||||
service.set_rate(ExchangeRate::new(2, "USD".to_string(), "GBP".to_string(), 0.75));
|
||||
service.set_rate(ExchangeRate::new(3, "USD".to_string(), "JPY".to_string(), 110.0));
|
||||
service.set_rate(ExchangeRate::new(4, "USD".to_string(), "CAD".to_string(), 1.25));
|
||||
service.set_rate(ExchangeRate::new(5, "USD".to_string(), "AUD".to_string(), 1.35));
|
||||
|
||||
service
|
||||
};
|
||||
}
|
577
herodb_old/src/models/biz/invoice.rs
Normal file
577
herodb_old/src/models/biz/invoice.rs
Normal file
@@ -0,0 +1,577 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc, Datelike};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// InvoiceStatus represents the status of an invoice
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum InvoiceStatus {
|
||||
Draft,
|
||||
Sent,
|
||||
Paid,
|
||||
Overdue,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// PaymentStatus represents the payment status of an invoice
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PaymentStatus {
|
||||
Unpaid,
|
||||
PartiallyPaid,
|
||||
Paid,
|
||||
}
|
||||
|
||||
/// Payment represents a payment made against an invoice
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Payment {
|
||||
pub amount: Currency,
|
||||
pub date: DateTime<Utc>,
|
||||
pub method: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
impl Payment {
|
||||
/// Create a new payment
|
||||
pub fn new(amount: Currency, method: String, comment: String) -> Self {
|
||||
Self {
|
||||
amount,
|
||||
date: Utc::now(),
|
||||
method,
|
||||
comment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// InvoiceItem represents an item in an invoice
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InvoiceItem {
|
||||
pub id: u32,
|
||||
pub invoice_id: u32,
|
||||
pub description: String,
|
||||
pub amount: Currency,
|
||||
pub service_id: Option<u32>,
|
||||
pub sale_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl InvoiceItem {
|
||||
/// Create a new invoice item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
invoice_id: u32,
|
||||
description: String,
|
||||
amount: Currency,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
invoice_id,
|
||||
description,
|
||||
amount,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Link the invoice item to a service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
}
|
||||
|
||||
/// Link the invoice item to a sale
|
||||
pub fn link_to_sale(&mut self, sale_id: u32) {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for InvoiceItem
|
||||
pub struct InvoiceItemBuilder {
|
||||
id: Option<u32>,
|
||||
invoice_id: Option<u32>,
|
||||
description: Option<String>,
|
||||
amount: Option<Currency>,
|
||||
service_id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl InvoiceItemBuilder {
|
||||
/// Create a new InvoiceItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
invoice_id: None,
|
||||
description: None,
|
||||
amount: None,
|
||||
service_id: None,
|
||||
sale_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the invoice_id
|
||||
pub fn invoice_id(mut self, invoice_id: u32) -> Self {
|
||||
self.invoice_id = Some(invoice_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the amount
|
||||
pub fn amount(mut self, amount: Currency) -> Self {
|
||||
self.amount = Some(amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self.service_id = None; // An invoice item can only be linked to either a service or a sale
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the InvoiceItem object
|
||||
pub fn build(self) -> Result<InvoiceItem, &'static str> {
|
||||
Ok(InvoiceItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
invoice_id: self.invoice_id.ok_or("invoice_id is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_id: self.sale_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoice represents an invoice sent to a customer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Invoice {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub total_amount: Currency,
|
||||
pub balance_due: Currency,
|
||||
pub status: InvoiceStatus,
|
||||
pub payment_status: PaymentStatus,
|
||||
pub issue_date: DateTime<Utc>,
|
||||
pub due_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<InvoiceItem>,
|
||||
pub payments: Vec<Payment>,
|
||||
}
|
||||
|
||||
impl Invoice {
|
||||
/// Create a new invoice with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
issue_date: DateTime<Utc>,
|
||||
due_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID for zero amounts
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
total_amount: zero_amount.clone(),
|
||||
balance_due: zero_amount,
|
||||
status: InvoiceStatus::Draft,
|
||||
payment_status: PaymentStatus::Unpaid,
|
||||
issue_date,
|
||||
due_date,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
payments: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the invoice and update the total amount
|
||||
pub fn add_item(&mut self, item: InvoiceItem) {
|
||||
// Make sure the item's invoice_id matches this invoice
|
||||
assert_eq!(self.id, item.invoice_id, "Item invoice_id must match invoice id");
|
||||
|
||||
// Update the total amount
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.amount.amount,
|
||||
item.amount.currency_code.clone()
|
||||
);
|
||||
self.balance_due = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.amount.amount,
|
||||
item.amount.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.amount.amount;
|
||||
self.balance_due.amount += item.amount.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the total amount based on all items
|
||||
pub fn calculate_total(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].amount.currency_code.clone();
|
||||
|
||||
// Calculate the total amount
|
||||
let mut total = 0.0;
|
||||
for item in &self.items {
|
||||
total += item.amount.amount;
|
||||
}
|
||||
|
||||
// Update the total amount
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
total,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Recalculate the balance due
|
||||
self.calculate_balance();
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Add a payment to the invoice and update the balance due and payment status
|
||||
pub fn add_payment(&mut self, payment: Payment) {
|
||||
// Update the balance due
|
||||
self.balance_due.amount -= payment.amount.amount;
|
||||
|
||||
// Add the payment to the list
|
||||
self.payments.push(payment);
|
||||
|
||||
// Update the payment status
|
||||
self.update_payment_status();
|
||||
|
||||
// Update the invoice timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the balance due based on total amount and payments
|
||||
pub fn calculate_balance(&mut self) {
|
||||
// Start with the total amount
|
||||
let mut balance = self.total_amount.amount;
|
||||
|
||||
// Subtract all payments
|
||||
for payment in &self.payments {
|
||||
balance -= payment.amount.amount;
|
||||
}
|
||||
|
||||
// Update the balance due
|
||||
self.balance_due = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
balance,
|
||||
self.total_amount.currency_code.clone()
|
||||
);
|
||||
|
||||
// Update the payment status
|
||||
self.update_payment_status();
|
||||
}
|
||||
|
||||
/// Update the payment status based on the balance due
|
||||
fn update_payment_status(&mut self) {
|
||||
if self.balance_due.amount <= 0.0 {
|
||||
self.payment_status = PaymentStatus::Paid;
|
||||
// If fully paid, also update the invoice status
|
||||
if self.status != InvoiceStatus::Cancelled {
|
||||
self.status = InvoiceStatus::Paid;
|
||||
}
|
||||
} else if self.payments.is_empty() {
|
||||
self.payment_status = PaymentStatus::Unpaid;
|
||||
} else {
|
||||
self.payment_status = PaymentStatus::PartiallyPaid;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the invoice
|
||||
pub fn update_status(&mut self, status: InvoiceStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
// If the invoice is cancelled, don't change the payment status
|
||||
if status != InvoiceStatus::Cancelled {
|
||||
// Re-evaluate the payment status
|
||||
self.update_payment_status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the invoice is overdue
|
||||
pub fn is_overdue(&self) -> bool {
|
||||
let now = Utc::now();
|
||||
self.payment_status != PaymentStatus::Paid &&
|
||||
now > self.due_date &&
|
||||
self.status != InvoiceStatus::Cancelled
|
||||
}
|
||||
|
||||
/// Mark the invoice as overdue if it's past the due date
|
||||
pub fn check_if_overdue(&mut self) -> bool {
|
||||
if self.is_overdue() && self.status != InvoiceStatus::Overdue {
|
||||
self.status = InvoiceStatus::Overdue;
|
||||
self.updated_at = Utc::now();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Invoice
|
||||
pub struct InvoiceBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
total_amount: Option<Currency>,
|
||||
balance_due: Option<Currency>,
|
||||
status: Option<InvoiceStatus>,
|
||||
payment_status: Option<PaymentStatus>,
|
||||
issue_date: Option<DateTime<Utc>>,
|
||||
due_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<InvoiceItem>,
|
||||
payments: Vec<Payment>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl InvoiceBuilder {
|
||||
/// Create a new InvoiceBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
total_amount: None,
|
||||
balance_due: None,
|
||||
status: None,
|
||||
payment_status: None,
|
||||
issue_date: None,
|
||||
due_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
payments: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: InvoiceStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the issue_date
|
||||
pub fn issue_date(mut self, issue_date: DateTime<Utc>) -> Self {
|
||||
self.issue_date = Some(issue_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the due_date
|
||||
pub fn due_date(mut self, due_date: DateTime<Utc>) -> Self {
|
||||
self.due_date = Some(due_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the invoice
|
||||
pub fn add_item(mut self, item: InvoiceItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a payment to the invoice
|
||||
pub fn add_payment(mut self, payment: Payment) -> Self {
|
||||
self.payments.push(payment);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Invoice object
|
||||
pub fn build(self) -> Result<Invoice, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount and balance due
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate total amount from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's invoice_id matches this invoice
|
||||
if item.invoice_id != id {
|
||||
return Err("Item invoice_id must match invoice id");
|
||||
}
|
||||
|
||||
total_amount.amount += item.amount.amount;
|
||||
}
|
||||
|
||||
// Calculate balance due (total minus payments)
|
||||
let mut balance_due = total_amount.clone();
|
||||
for payment in &self.payments {
|
||||
balance_due.amount -= payment.amount.amount;
|
||||
}
|
||||
|
||||
// Determine payment status
|
||||
let payment_status = if balance_due.amount <= 0.0 {
|
||||
PaymentStatus::Paid
|
||||
} else if self.payments.is_empty() {
|
||||
PaymentStatus::Unpaid
|
||||
} else {
|
||||
PaymentStatus::PartiallyPaid
|
||||
};
|
||||
|
||||
// Determine invoice status if not provided
|
||||
let status = if let Some(status) = self.status {
|
||||
status
|
||||
} else if payment_status == PaymentStatus::Paid {
|
||||
InvoiceStatus::Paid
|
||||
} else {
|
||||
InvoiceStatus::Draft
|
||||
};
|
||||
|
||||
Ok(Invoice {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
balance_due: self.balance_due.unwrap_or(balance_due),
|
||||
status,
|
||||
payment_status,
|
||||
issue_date: self.issue_date.ok_or("issue_date is required")?,
|
||||
due_date: self.due_date.ok_or("due_date is required")?,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
payments: self.payments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Storable for Invoice {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Invoice {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"invoice"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for payment_status
|
||||
keys.push(IndexKey {
|
||||
name: "payment_status",
|
||||
value: format!("{:?}", self.payment_status),
|
||||
});
|
||||
|
||||
// Add an index for currency code
|
||||
keys.push(IndexKey {
|
||||
name: "currency",
|
||||
value: self.total_amount.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add an index for amount range
|
||||
let amount_range = match self.total_amount.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "amount_range",
|
||||
value: amount_range.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for issue date (year-month)
|
||||
keys.push(IndexKey {
|
||||
name: "issue_date",
|
||||
value: format!("{}", self.issue_date.format("%Y-%m")),
|
||||
});
|
||||
|
||||
// Add an index for due date (year-month)
|
||||
keys.push(IndexKey {
|
||||
name: "due_date",
|
||||
value: format!("{}", self.due_date.format("%Y-%m")),
|
||||
});
|
||||
|
||||
// Add an index for overdue invoices
|
||||
if self.is_overdue() {
|
||||
keys.push(IndexKey {
|
||||
name: "overdue",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
30
herodb_old/src/models/biz/lib.rs
Normal file
30
herodb_old/src/models/biz/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
pub mod user;
|
||||
pub mod vote;
|
||||
pub mod company;
|
||||
pub mod meeting;
|
||||
pub mod product;
|
||||
pub mod sale;
|
||||
pub mod shareholder;
|
||||
// pub mod db; // Moved to src/zaz/db
|
||||
// pub mod migration; // Removed
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use user::User;
|
||||
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub use meeting::Meeting;
|
||||
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
||||
pub use sale::Sale;
|
||||
pub use shareholder::Shareholder;
|
||||
|
||||
// Re-export builder types
|
||||
pub use product::{ProductBuilder, ProductComponentBuilder};
|
||||
pub use sale::{SaleBuilder, SaleItemBuilder};
|
||||
|
||||
// Re-export Currency and its builder
|
||||
pub use product::Currency;
|
||||
pub use currency::CurrencyBuilder;
|
||||
|
||||
// Re-export database components
|
||||
// Re-export database components from db module
|
||||
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};
|
28
herodb_old/src/models/biz/mod.rs
Normal file
28
herodb_old/src/models/biz/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
pub mod currency;
|
||||
pub mod product;
|
||||
pub mod sale;
|
||||
pub mod exchange_rate;
|
||||
pub mod service;
|
||||
pub mod customer;
|
||||
pub mod contract;
|
||||
pub mod invoice;
|
||||
|
||||
// Re-export all model types for convenience
|
||||
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||
pub use currency::Currency;
|
||||
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
|
||||
pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency};
|
||||
pub use customer::Customer;
|
||||
pub use contract::{Contract, ContractStatus};
|
||||
pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment};
|
||||
|
||||
// Re-export builder types
|
||||
pub use product::{ProductBuilder, ProductComponentBuilder};
|
||||
pub use sale::{SaleBuilder, SaleItemBuilder};
|
||||
pub use currency::CurrencyBuilder;
|
||||
pub use exchange_rate::ExchangeRateBuilder;
|
||||
pub use service::{ServiceBuilder, ServiceItemBuilder};
|
||||
pub use customer::CustomerBuilder;
|
||||
pub use contract::ContractBuilder;
|
||||
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};
|
425
herodb_old/src/models/biz/product.rs
Normal file
425
herodb_old/src/models/biz/product.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use crate::db::model::{Model, IndexKey};
|
||||
use crate::db::Storable;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ProductType represents the type of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProductType {
|
||||
Product,
|
||||
Service,
|
||||
}
|
||||
|
||||
/// ProductStatus represents the status of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProductStatus {
|
||||
Available,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// ProductComponent represents a component of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProductComponent {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub quantity: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ProductComponent {
|
||||
/// Create a new product component with default timestamps
|
||||
pub fn new(id: u32, name: String, description: String, quantity: i64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
quantity,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for ProductComponent
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductComponentBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
quantity: Option<i64>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ProductComponentBuilder {
|
||||
/// Create a new ProductComponentBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
quantity: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i64) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the created_at timestamp
|
||||
pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
|
||||
self.created_at = Some(created_at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the updated_at timestamp
|
||||
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
|
||||
self.updated_at = Some(updated_at);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ProductComponent object
|
||||
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
let now = Utc::now();
|
||||
Ok(ProductComponent {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
quantity: self.quantity.ok_or("quantity is required")?,
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Product represents a product or service offered in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Product {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: Currency,
|
||||
pub type_: ProductType,
|
||||
pub category: String,
|
||||
pub status: ProductStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub max_amount: i64, // means allows us to define how many max of this there are
|
||||
pub purchase_till: DateTime<Utc>,
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
pub components: Vec<ProductComponent>,
|
||||
}
|
||||
|
||||
impl Product {
|
||||
/// Create a new product with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: i64,
|
||||
validity_days: i64, // How many days the product is valid after purchase
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
// Default: purchasable for 1 year, active for specified validity days after purchase
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
type_,
|
||||
category,
|
||||
status,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
max_amount,
|
||||
purchase_till: now + Duration::days(365),
|
||||
active_till: now + Duration::days(validity_days),
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a component to this product
|
||||
pub fn add_component(&mut self, component: ProductComponent) {
|
||||
self.components.push(component);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the purchase availability timeframe
|
||||
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
|
||||
self.purchase_till = purchase_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the active timeframe
|
||||
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
|
||||
self.active_till = active_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Check if the product is available for purchase
|
||||
pub fn is_purchasable(&self) -> bool {
|
||||
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||
}
|
||||
|
||||
/// Check if the product is still active (for services)
|
||||
pub fn is_active(&self) -> bool {
|
||||
Utc::now() <= self.active_till
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Product
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<Currency>,
|
||||
type_: Option<ProductType>,
|
||||
category: Option<String>,
|
||||
status: Option<ProductStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
max_amount: Option<i64>,
|
||||
purchase_till: Option<DateTime<Utc>>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
components: Vec<ProductComponent>,
|
||||
validity_days: Option<i64>,
|
||||
}
|
||||
|
||||
impl ProductBuilder {
|
||||
/// Create a new ProductBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
price: None,
|
||||
type_: None,
|
||||
category: None,
|
||||
status: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
max_amount: None,
|
||||
purchase_till: None,
|
||||
active_till: None,
|
||||
components: Vec::new(),
|
||||
validity_days: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the price
|
||||
pub fn price(mut self, price: Currency) -> Self {
|
||||
self.price = Some(price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product type
|
||||
pub fn type_(mut self, type_: ProductType) -> Self {
|
||||
self.type_ = Some(type_);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the category
|
||||
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
|
||||
self.category = Some(category.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ProductStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max amount
|
||||
pub fn max_amount(mut self, max_amount: i64) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the validity days
|
||||
pub fn validity_days(mut self, validity_days: i64) -> Self {
|
||||
self.validity_days = Some(validity_days);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the purchase_till date directly
|
||||
pub fn purchase_till(mut self, purchase_till: DateTime<Utc>) -> Self {
|
||||
self.purchase_till = Some(purchase_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till date directly
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a component to the product
|
||||
pub fn add_component(mut self, component: ProductComponent) -> Self {
|
||||
self.components.push(component);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Product object
|
||||
pub fn build(self) -> Result<Product, &'static str> {
|
||||
let now = Utc::now();
|
||||
let created_at = self.created_at.unwrap_or(now);
|
||||
let updated_at = self.updated_at.unwrap_or(now);
|
||||
|
||||
// Calculate purchase_till and active_till based on validity_days if not set directly
|
||||
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
|
||||
let active_till = if let Some(validity_days) = self.validity_days {
|
||||
self.active_till
|
||||
.unwrap_or(now + Duration::days(validity_days))
|
||||
} else {
|
||||
self.active_till
|
||||
.ok_or("Either active_till or validity_days must be provided")?
|
||||
};
|
||||
|
||||
Ok(Product {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
price: self.price.ok_or("price is required")?,
|
||||
type_: self.type_.ok_or("type_ is required")?,
|
||||
category: self.category.ok_or("category is required")?,
|
||||
status: self.status.ok_or("status is required")?,
|
||||
created_at,
|
||||
updated_at,
|
||||
max_amount: self.max_amount.ok_or("max_amount is required")?,
|
||||
purchase_till,
|
||||
active_till,
|
||||
components: self.components,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Product {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Product {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"product"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for name
|
||||
keys.push(IndexKey {
|
||||
name: "name",
|
||||
value: self.name.clone(),
|
||||
});
|
||||
|
||||
// Add an index for category
|
||||
keys.push(IndexKey {
|
||||
name: "category",
|
||||
value: self.category.clone(),
|
||||
});
|
||||
|
||||
// Add an index for product type
|
||||
keys.push(IndexKey {
|
||||
name: "type",
|
||||
value: format!("{:?}", self.type_),
|
||||
});
|
||||
|
||||
// Add an index for status
|
||||
keys.push(IndexKey {
|
||||
name: "status",
|
||||
value: format!("{:?}", self.status),
|
||||
});
|
||||
|
||||
// Add an index for price range
|
||||
let price_range = match self.price.amount {
|
||||
a if a < 100.0 => "0-100",
|
||||
a if a < 1000.0 => "100-1000",
|
||||
a if a < 10000.0 => "1000-10000",
|
||||
_ => "10000+",
|
||||
};
|
||||
|
||||
keys.push(IndexKey {
|
||||
name: "price_range",
|
||||
value: price_range.to_string(),
|
||||
});
|
||||
|
||||
// Add an index for currency code
|
||||
keys.push(IndexKey {
|
||||
name: "currency",
|
||||
value: self.price.currency_code.clone(),
|
||||
});
|
||||
|
||||
// Add indexes for purchasable and active products
|
||||
if self.is_purchasable() {
|
||||
keys.push(IndexKey {
|
||||
name: "purchasable",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if self.is_active() {
|
||||
keys.push(IndexKey {
|
||||
name: "active",
|
||||
value: "true".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
||||
// Import Currency from the currency module
|
||||
use crate::models::biz::Currency;
|
2164
herodb_old/src/models/biz/rhai/Cargo.lock
generated
Normal file
2164
herodb_old/src/models/biz/rhai/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
herodb_old/src/models/biz/rhai/Cargo.toml
Normal file
19
herodb_old/src/models/biz/rhai/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "biz_rhai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rhai = "1.21.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4"
|
||||
herodb = { path = "../../../.." }
|
||||
|
||||
[lib]
|
||||
name = "biz_rhai"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "example"
|
||||
path = "examples/example.rs"
|
168
herodb_old/src/models/biz/rhai/examples/example.rhai
Normal file
168
herodb_old/src/models/biz/rhai/examples/example.rhai
Normal file
@@ -0,0 +1,168 @@
|
||||
// Example script demonstrating the use of Business module operations
|
||||
|
||||
// Custom repeat function since Rhai doesn't have a built-in repeat method
|
||||
fn repeat_str(str, count) {
|
||||
let result = "";
|
||||
for i in 0..count {
|
||||
result += str;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Print a section header
|
||||
fn print_section(title) {
|
||||
let line = repeat_str("=", 50);
|
||||
print(line);
|
||||
print(" " + title);
|
||||
print(line);
|
||||
}
|
||||
|
||||
print_section("BUSINESS MODULE OPERATIONS EXAMPLE");
|
||||
|
||||
// Currency Operations
|
||||
print_section("CURRENCY OPERATIONS");
|
||||
|
||||
// Create a currency
|
||||
print("\nCreating currencies...");
|
||||
let usd = create_currency(100.0, "USD");
|
||||
print("USD Currency created: " + usd.amount + " " + usd.currency_code);
|
||||
|
||||
// Convert currencies
|
||||
print("\nConverting currencies...");
|
||||
let eur = create_currency(150.0, "EUR");
|
||||
print("EUR Currency created: " + eur.amount + " " + eur.currency_code);
|
||||
|
||||
let eur_to_usd = convert_currency(eur, "USD");
|
||||
print("EUR to USD: " + eur.amount + " EUR = " + eur_to_usd.amount + " USD");
|
||||
|
||||
// Product Component Operations
|
||||
print_section("PRODUCT COMPONENT OPERATIONS");
|
||||
|
||||
// Create a product component
|
||||
print("\nCreating product components...");
|
||||
let component1 = create_product_component(1, "CPU", "Intel i7 Processor", 1);
|
||||
print("Component created: " + component1.name);
|
||||
print(" ID: " + component1.id);
|
||||
print(" Description: " + component1.description);
|
||||
print(" Quantity: " + component1.quantity);
|
||||
|
||||
// Create another component
|
||||
let component2 = create_product_component(2, "RAM", "32GB DDR4 Memory", 2);
|
||||
print("Component created: " + component2.name);
|
||||
print(" ID: " + component2.id);
|
||||
print(" Description: " + component2.description);
|
||||
print(" Quantity: " + component2.quantity);
|
||||
|
||||
// Product Operations
|
||||
print_section("PRODUCT OPERATIONS");
|
||||
|
||||
// Create a product using builder pattern
|
||||
print("\nCreating a product...");
|
||||
let product = create_product_builder()
|
||||
.product_name("High-End Gaming PC")
|
||||
.product_description("Ultimate gaming experience")
|
||||
.product_price(create_currency(1999.99, "USD"))
|
||||
.product_validity_days(365)
|
||||
.build();
|
||||
|
||||
print("Product created: " + product.name);
|
||||
print(" ID: " + product.id);
|
||||
print(" Description: " + product.description);
|
||||
print(" Price: " + product.price.amount + " " + product.price.currency_code);
|
||||
|
||||
// Add components to the product
|
||||
print("\nAdding components to product...");
|
||||
let product_with_components = product
|
||||
.add_component(component1)
|
||||
.add_component(component2);
|
||||
|
||||
print("Components added to product");
|
||||
|
||||
// Get components from the product
|
||||
let components = get_product_components(product_with_components);
|
||||
print("Number of components: " + components.len());
|
||||
|
||||
// Customer Operations
|
||||
print_section("CUSTOMER OPERATIONS");
|
||||
|
||||
// Create a customer
|
||||
print("\nCreating a customer...");
|
||||
let customer = create_customer("John Doe", "john@example.com", "+1234567890", "123 Main St");
|
||||
print("Customer created: " + customer.name);
|
||||
print(" Email: " + customer.email);
|
||||
print(" Phone: " + customer.phone);
|
||||
print(" Address: " + customer.address);
|
||||
|
||||
// Sale Operations
|
||||
print_section("SALE OPERATIONS");
|
||||
|
||||
// Create a sale
|
||||
print("\nCreating a sale...");
|
||||
let sale = create_sale(customer, "2025-04-19");
|
||||
print("Sale created for customer: " + sale.customer.name);
|
||||
print(" Date: " + sale.date);
|
||||
|
||||
// Add product to sale
|
||||
let sale_with_item = add_sale_item(sale, product_with_components, 1);
|
||||
print("Added product to sale: " + product_with_components.name);
|
||||
|
||||
// Service Operations
|
||||
print_section("SERVICE OPERATIONS");
|
||||
|
||||
// Create a service
|
||||
print("\nCreating a service...");
|
||||
let service = create_service(
|
||||
"Premium Support",
|
||||
"24/7 Technical Support",
|
||||
create_currency(49.99, "USD"),
|
||||
30
|
||||
);
|
||||
print("Service created: " + service.name);
|
||||
print(" Description: " + service.description);
|
||||
print(" Price: " + service.price.amount + " " + service.price.currency_code);
|
||||
print(" Duration: " + service.duration + " days");
|
||||
|
||||
// Contract Operations
|
||||
print_section("CONTRACT OPERATIONS");
|
||||
|
||||
// Create a contract
|
||||
print("\nCreating a contract...");
|
||||
let contract = create_contract(
|
||||
"Support Agreement",
|
||||
"Annual support contract",
|
||||
"2025-04-19",
|
||||
"2026-04-19",
|
||||
create_currency(599.99, "USD"),
|
||||
"Active"
|
||||
);
|
||||
print("Contract created: " + contract.title);
|
||||
print(" Description: " + contract.description);
|
||||
print(" Start Date: " + contract.start_date);
|
||||
print(" End Date: " + contract.end_date);
|
||||
print(" Value: " + contract.value.amount + " " + contract.value.currency_code);
|
||||
print(" Status: " + contract.status);
|
||||
|
||||
// Invoice Operations
|
||||
print_section("INVOICE OPERATIONS");
|
||||
|
||||
// Create an invoice
|
||||
print("\nCreating an invoice...");
|
||||
let invoice = create_invoice(
|
||||
"INV-2025-001",
|
||||
"2025-04-19",
|
||||
"2025-05-19",
|
||||
customer,
|
||||
create_currency(2499.99, "USD"),
|
||||
"Issued",
|
||||
"Pending"
|
||||
);
|
||||
print("Invoice created: " + invoice.number);
|
||||
print(" Date: " + invoice.date);
|
||||
print(" Due Date: " + invoice.due_date);
|
||||
print(" Customer: " + invoice.customer.name);
|
||||
print(" Amount: " + invoice.amount.amount + " " + invoice.amount.currency_code);
|
||||
print(" Status: " + invoice.status);
|
||||
print(" Payment Status: " + invoice.payment_status);
|
||||
|
||||
print_section("EXAMPLE COMPLETED");
|
||||
print("All business module operations completed successfully!");
|
41
herodb_old/src/models/biz/rhai/examples/example.rs
Normal file
41
herodb_old/src/models/biz/rhai/examples/example.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::{fs, path::Path};
|
||||
use biz_rhai::create_rhai_engine;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Business Module Rhai Wrapper Example ===");
|
||||
|
||||
// Create a Rhai engine with business module functionality
|
||||
let mut engine = create_rhai_engine();
|
||||
println!("Successfully created Rhai engine");
|
||||
|
||||
// Get the path to the example.rhai script
|
||||
let script_path = get_script_path()?;
|
||||
println!("Loading script from: {}", script_path.display());
|
||||
|
||||
// Load the script content
|
||||
let script = fs::read_to_string(&script_path)
|
||||
.map_err(|e| format!("Failed to read script file: {}", e))?;
|
||||
|
||||
// Run the script
|
||||
println!("\n=== Running Rhai script ===");
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("\nScript executed successfully!"),
|
||||
Err(e) => println!("\nScript execution error: {}", e),
|
||||
}
|
||||
|
||||
println!("\nExample completed!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_script_path() -> Result<std::path::PathBuf, String> {
|
||||
// When running with cargo run --example, the script will be in the examples directory
|
||||
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("examples")
|
||||
.join("example.rhai");
|
||||
|
||||
if script_path.exists() {
|
||||
Ok(script_path)
|
||||
} else {
|
||||
Err(format!("Could not find example.rhai script at {}", script_path.display()))
|
||||
}
|
||||
}
|
88
herodb_old/src/models/biz/rhai/src/engine.rs
Normal file
88
herodb_old/src/models/biz/rhai/src/engine.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use rhai::{Engine, EvalAltResult, Map, Dynamic, Array};
|
||||
use crate::wrapper::*;
|
||||
use crate::generic_wrapper::ToRhai;
|
||||
|
||||
/// Create a new Rhai engine with business module functionality
|
||||
pub fn create_rhai_engine() -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register business module types and functions
|
||||
register_business_types(&mut engine);
|
||||
|
||||
engine
|
||||
}
|
||||
|
||||
/// Register business module types and functions
|
||||
fn register_business_types(engine: &mut Engine) {
|
||||
// Currency functions
|
||||
engine.register_fn("create_currency", create_currency);
|
||||
engine.register_fn("convert_currency", convert_currency);
|
||||
|
||||
// Product functions
|
||||
engine.register_fn("create_product_builder", create_product_builder);
|
||||
engine.register_fn("product_name", product_builder_name);
|
||||
engine.register_fn("product_description", product_builder_description);
|
||||
engine.register_fn("product_price", product_builder_price);
|
||||
engine.register_fn("product_validity_days", product_builder_validity_days);
|
||||
engine.register_fn("add_component", product_builder_add_component);
|
||||
engine.register_fn("build", product_builder_build);
|
||||
|
||||
// Product component functions
|
||||
engine.register_fn("create_product_component", create_product_component);
|
||||
engine.register_fn("component_name", product_component_name);
|
||||
engine.register_fn("component_description", product_component_description);
|
||||
engine.register_fn("component_quantity", product_component_quantity);
|
||||
|
||||
// Sale functions
|
||||
engine.register_fn("create_sale", create_sale);
|
||||
engine.register_fn("add_sale_item", add_sale_item);
|
||||
engine.register_fn("sale_customer", sale_customer);
|
||||
engine.register_fn("sale_date", sale_date);
|
||||
engine.register_fn("sale_status", sale_status);
|
||||
|
||||
// Customer functions
|
||||
engine.register_fn("create_customer", create_customer);
|
||||
engine.register_fn("customer_name", customer_name);
|
||||
engine.register_fn("customer_email", customer_email);
|
||||
engine.register_fn("customer_phone", customer_phone);
|
||||
engine.register_fn("customer_address", customer_address);
|
||||
|
||||
// Service functions
|
||||
engine.register_fn("create_service", create_service);
|
||||
engine.register_fn("service_name", service_name);
|
||||
engine.register_fn("service_description", service_description);
|
||||
engine.register_fn("service_price", service_price);
|
||||
engine.register_fn("service_duration", service_duration);
|
||||
|
||||
// Contract functions
|
||||
engine.register_fn("create_contract", create_contract);
|
||||
engine.register_fn("contract_title", contract_title);
|
||||
engine.register_fn("contract_description", contract_description);
|
||||
engine.register_fn("contract_start_date", contract_start_date);
|
||||
engine.register_fn("contract_end_date", contract_end_date);
|
||||
engine.register_fn("contract_value", contract_value);
|
||||
engine.register_fn("contract_status", contract_status);
|
||||
|
||||
// Invoice functions
|
||||
engine.register_fn("create_invoice", create_invoice);
|
||||
engine.register_fn("invoice_number", invoice_number);
|
||||
engine.register_fn("invoice_date", invoice_date);
|
||||
engine.register_fn("invoice_due_date", invoice_due_date);
|
||||
engine.register_fn("invoice_customer", invoice_customer);
|
||||
engine.register_fn("invoice_amount", invoice_amount);
|
||||
engine.register_fn("invoice_status", invoice_status);
|
||||
engine.register_fn("invoice_payment_status", invoice_payment_status);
|
||||
|
||||
// Helper function to get components from a product
|
||||
engine.register_fn("get_product_components", |product_map: Map| -> Array {
|
||||
let mut array = Array::new();
|
||||
|
||||
if let Some(components) = product_map.get("components") {
|
||||
if let Some(components_array) = components.clone().try_cast::<Array>() {
|
||||
return components_array;
|
||||
}
|
||||
}
|
||||
|
||||
array
|
||||
});
|
||||
}
|
132
herodb_old/src/models/biz/rhai/src/generic_wrapper.rs
Normal file
132
herodb_old/src/models/biz/rhai/src/generic_wrapper.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::collections::HashMap;
|
||||
use rhai::{Dynamic, Map, Array};
|
||||
|
||||
/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations
|
||||
pub trait ToRhai {
|
||||
/// Convert to a Rhai Dynamic value
|
||||
fn to_rhai(&self) -> Dynamic;
|
||||
}
|
||||
|
||||
// Implementation of ToRhai for Dynamic
|
||||
impl ToRhai for Dynamic {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic trait for wrapping Rust functions to be used with Rhai
|
||||
pub trait RhaiWrapper {
|
||||
/// Wrap a function that takes ownership of self
|
||||
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(Self) -> R,
|
||||
R: ToRhai;
|
||||
|
||||
/// Wrap a function that takes a mutable reference to self
|
||||
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
R: ToRhai;
|
||||
|
||||
/// Wrap a function that takes an immutable reference to self
|
||||
fn wrap<F, R>(&self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&Self) -> R,
|
||||
R: ToRhai;
|
||||
}
|
||||
|
||||
/// Implementation of RhaiWrapper for any type
|
||||
impl<T> RhaiWrapper for T {
|
||||
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
|
||||
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
|
||||
fn wrap<F, R>(&self, f: F) -> Dynamic
|
||||
where
|
||||
Self: Sized + Clone,
|
||||
F: FnOnce(&Self) -> R,
|
||||
R: ToRhai,
|
||||
{
|
||||
let result = f(self);
|
||||
result.to_rhai()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Rhai Map to a Rust HashMap
|
||||
pub fn map_to_hashmap(map: &Map) -> HashMap<String, String> {
|
||||
let mut result = HashMap::new();
|
||||
for (key, value) in map.iter() {
|
||||
let k = key.clone().to_string();
|
||||
let v = value.clone().to_string();
|
||||
if !k.is_empty() && !v.is_empty() {
|
||||
result.insert(k, v);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a HashMap<String, String> to a Rhai Map
|
||||
pub fn hashmap_to_map(map: &HashMap<String, String>) -> Map {
|
||||
let mut result = Map::new();
|
||||
for (key, value) in map.iter() {
|
||||
result.insert(key.clone().into(), Dynamic::from(value.clone()));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Convert a Rhai Array to a Vec of strings
|
||||
pub fn array_to_vec_string(array: &Array) -> Vec<String> {
|
||||
array.iter()
|
||||
.filter_map(|item| {
|
||||
let s = item.clone().to_string();
|
||||
if !s.is_empty() { Some(s) } else { None }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<String>
|
||||
pub fn dynamic_to_string_option(value: &Dynamic) -> Option<String> {
|
||||
if value.is_string() {
|
||||
Some(value.clone().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<u32>
|
||||
pub fn dynamic_to_u32_option(value: &Dynamic) -> Option<u32> {
|
||||
if value.is_int() {
|
||||
Some(value.as_int().unwrap() as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to convert Dynamic to Option<&str> with lifetime management
|
||||
pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> {
|
||||
if value.is_string() {
|
||||
*storage = value.clone().to_string();
|
||||
Some(storage.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
11
herodb_old/src/models/biz/rhai/src/lib.rs
Normal file
11
herodb_old/src/models/biz/rhai/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// Re-export the utility modules
|
||||
pub mod generic_wrapper;
|
||||
pub mod wrapper;
|
||||
pub mod engine;
|
||||
|
||||
// Re-export the utility traits and functions
|
||||
pub use generic_wrapper::{RhaiWrapper, map_to_hashmap, array_to_vec_string,
|
||||
dynamic_to_string_option, hashmap_to_map};
|
||||
pub use engine::create_rhai_engine;
|
||||
|
||||
// The create_rhai_engine function is now in the engine module
|
640
herodb_old/src/models/biz/rhai/src/wrapper.rs
Normal file
640
herodb_old/src/models/biz/rhai/src/wrapper.rs
Normal file
@@ -0,0 +1,640 @@
|
||||
//! Rhai wrappers for Business module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Business module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map, Position};
|
||||
use std::collections::HashMap;
|
||||
use crate::generic_wrapper::{ToRhai, RhaiWrapper, map_to_hashmap, dynamic_to_string_option, dynamic_to_u32_option};
|
||||
|
||||
// Import business module types
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use herodb::models::biz::{
|
||||
Currency, CurrencyBuilder,
|
||||
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
|
||||
Customer, CustomerBuilder,
|
||||
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
|
||||
Service, ServiceBuilder, ServiceItem, ServiceItemBuilder, ServiceStatus, BillingFrequency,
|
||||
ExchangeRate, ExchangeRateBuilder, ExchangeRateService, EXCHANGE_RATE_SERVICE,
|
||||
Contract, ContractBuilder, ContractStatus,
|
||||
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment
|
||||
};
|
||||
|
||||
// Business module ToRhai implementations
|
||||
|
||||
// Currency ToRhai implementation
|
||||
impl ToRhai for Currency {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("amount".into(), Dynamic::from(self.amount));
|
||||
map.insert("currency_code".into(), Dynamic::from(self.currency_code.clone()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductType ToRhai implementation
|
||||
impl ToRhai for ProductType {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ProductType::Product => "Product",
|
||||
ProductType::Service => "Service",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductStatus ToRhai implementation
|
||||
impl ToRhai for ProductStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ProductStatus::Active => "Active",
|
||||
ProductStatus::Error => "Error",
|
||||
ProductStatus::EndOfLife => "EndOfLife",
|
||||
ProductStatus::Paused => "Paused",
|
||||
ProductStatus::Available => "Available",
|
||||
ProductStatus::Unavailable => "Unavailable",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ProductComponent ToRhai implementation
|
||||
impl ToRhai for ProductComponent {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
map.insert("energy_usage".into(), Dynamic::from(self.energy_usage));
|
||||
map.insert("cost".into(), self.cost.to_rhai());
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Product ToRhai implementation
|
||||
impl ToRhai for Product {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("type".into(), self.type_.to_rhai());
|
||||
map.insert("category".into(), Dynamic::from(self.category.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
map.insert("max_amount".into(), Dynamic::from(self.max_amount));
|
||||
map.insert("purchase_till".into(), Dynamic::from(self.purchase_till.to_string()));
|
||||
map.insert("active_till".into(), Dynamic::from(self.active_till.to_string()));
|
||||
|
||||
// Convert components to an array
|
||||
let components_array: Array = self.components.iter()
|
||||
.map(|component| component.to_rhai())
|
||||
.collect();
|
||||
map.insert("components".into(), Dynamic::from(components_array));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// SaleStatus ToRhai implementation
|
||||
impl ToRhai for SaleStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
SaleStatus::Pending => "Pending",
|
||||
SaleStatus::Completed => "Completed",
|
||||
SaleStatus::Cancelled => "Cancelled",
|
||||
SaleStatus::Refunded => "Refunded",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// SaleItem ToRhai implementation
|
||||
impl ToRhai for SaleItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("product_id".into(), Dynamic::from(self.product_id));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("discount".into(), Dynamic::from(self.discount));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Sale ToRhai implementation
|
||||
impl ToRhai for Sale {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Customer ToRhai implementation
|
||||
impl ToRhai for Customer {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("email".into(), Dynamic::from(self.email.clone()));
|
||||
map.insert("phone".into(), Dynamic::from(self.phone.clone()));
|
||||
map.insert("address".into(), Dynamic::from(self.address.clone()));
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStatus ToRhai implementation
|
||||
impl ToRhai for ServiceStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ServiceStatus::Active => "Active",
|
||||
ServiceStatus::Inactive => "Inactive",
|
||||
ServiceStatus::Pending => "Pending",
|
||||
ServiceStatus::Cancelled => "Cancelled",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// BillingFrequency ToRhai implementation
|
||||
impl ToRhai for BillingFrequency {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
BillingFrequency::OneTime => "OneTime",
|
||||
BillingFrequency::Daily => "Daily",
|
||||
BillingFrequency::Weekly => "Weekly",
|
||||
BillingFrequency::Monthly => "Monthly",
|
||||
BillingFrequency::Quarterly => "Quarterly",
|
||||
BillingFrequency::Yearly => "Yearly",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceItem ToRhai implementation
|
||||
impl ToRhai for ServiceItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("price".into(), self.price.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Service ToRhai implementation
|
||||
impl ToRhai for Service {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("name".into(), Dynamic::from(self.name.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
|
||||
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
|
||||
map.insert("billing_frequency".into(), self.billing_frequency.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ContractStatus ToRhai implementation
|
||||
impl ToRhai for ContractStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
ContractStatus::Draft => "Draft",
|
||||
ContractStatus::Pending => "Pending",
|
||||
ContractStatus::Active => "Active",
|
||||
ContractStatus::Completed => "Completed",
|
||||
ContractStatus::Cancelled => "Cancelled",
|
||||
ContractStatus::Expired => "Expired",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Contract ToRhai implementation
|
||||
impl ToRhai for Contract {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("title".into(), Dynamic::from(self.title.clone()));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
|
||||
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
|
||||
map.insert("value".into(), self.value.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// InvoiceStatus ToRhai implementation
|
||||
impl ToRhai for InvoiceStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
InvoiceStatus::Draft => "Draft",
|
||||
InvoiceStatus::Sent => "Sent",
|
||||
InvoiceStatus::Paid => "Paid",
|
||||
InvoiceStatus::Overdue => "Overdue",
|
||||
InvoiceStatus::Cancelled => "Cancelled",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// PaymentStatus ToRhai implementation
|
||||
impl ToRhai for PaymentStatus {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let value = match self {
|
||||
PaymentStatus::Pending => "Pending",
|
||||
PaymentStatus::Completed => "Completed",
|
||||
PaymentStatus::Failed => "Failed",
|
||||
PaymentStatus::Refunded => "Refunded",
|
||||
};
|
||||
Dynamic::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Payment ToRhai implementation
|
||||
impl ToRhai for Payment {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("amount".into(), self.amount.to_rhai());
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("method".into(), Dynamic::from(self.method.clone()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
map.insert("reference".into(), Dynamic::from(self.reference.clone()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// InvoiceItem ToRhai implementation
|
||||
impl ToRhai for InvoiceItem {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("description".into(), Dynamic::from(self.description.clone()));
|
||||
map.insert("quantity".into(), Dynamic::from(self.quantity));
|
||||
map.insert("unit_price".into(), self.unit_price.to_rhai());
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// Invoice ToRhai implementation
|
||||
impl ToRhai for Invoice {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("id".into(), Dynamic::from(self.id));
|
||||
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
map.insert("due_date".into(), Dynamic::from(self.due_date.to_string()));
|
||||
map.insert("status".into(), self.status.to_rhai());
|
||||
|
||||
// Convert items to an array
|
||||
let items_array: Array = self.items.iter()
|
||||
.map(|item| item.to_rhai())
|
||||
.collect();
|
||||
map.insert("items".into(), Dynamic::from(items_array));
|
||||
|
||||
// Convert payments to an array
|
||||
let payments_array: Array = self.payments.iter()
|
||||
.map(|payment| payment.to_rhai())
|
||||
.collect();
|
||||
map.insert("payments".into(), Dynamic::from(payments_array));
|
||||
|
||||
map.insert("subtotal".into(), self.subtotal.to_rhai());
|
||||
map.insert("tax".into(), self.tax.to_rhai());
|
||||
map.insert("total".into(), self.total.to_rhai());
|
||||
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
|
||||
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
|
||||
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
// ExchangeRate ToRhai implementation
|
||||
impl ToRhai for ExchangeRate {
|
||||
fn to_rhai(&self) -> Dynamic {
|
||||
let mut map = Map::new();
|
||||
map.insert("from_currency".into(), Dynamic::from(self.from_currency.clone()));
|
||||
map.insert("to_currency".into(), Dynamic::from(self.to_currency.clone()));
|
||||
map.insert("rate".into(), Dynamic::from(self.rate));
|
||||
map.insert("date".into(), Dynamic::from(self.date.to_string()));
|
||||
Dynamic::from_map(map)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Business Module Function Wrappers
|
||||
//
|
||||
|
||||
// Currency Functions
|
||||
pub fn currency_new(amount: f64, currency_code: &str) -> Currency {
|
||||
Currency::new(amount, currency_code.to_string())
|
||||
}
|
||||
|
||||
pub fn currency_to_usd(currency: &Currency) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match currency.to_usd() {
|
||||
Some(usd) => Ok(usd.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to convert currency to USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn currency_to_currency(currency: &Currency, target_currency: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match currency.to_currency(target_currency) {
|
||||
Some(converted) => Ok(converted.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert currency to {}", target_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// CurrencyBuilder Functions
|
||||
pub fn currency_builder_new() -> CurrencyBuilder {
|
||||
CurrencyBuilder::new()
|
||||
}
|
||||
|
||||
pub fn currency_builder_amount(builder: CurrencyBuilder, amount: f64) -> CurrencyBuilder {
|
||||
builder.amount(amount)
|
||||
}
|
||||
|
||||
pub fn currency_builder_currency_code(builder: CurrencyBuilder, currency_code: &str) -> CurrencyBuilder {
|
||||
builder.currency_code(currency_code)
|
||||
}
|
||||
|
||||
pub fn currency_builder_build(builder: CurrencyBuilder) -> Result<Currency, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// ProductComponent Functions
|
||||
pub fn product_component_new(id: i64, name: &str, description: &str, quantity: i64) -> ProductComponent {
|
||||
ProductComponent::new(id as u32, name.to_string(), description.to_string(), quantity as i32)
|
||||
}
|
||||
|
||||
pub fn product_component_total_energy_usage(component: &ProductComponent) -> f64 {
|
||||
component.total_energy_usage()
|
||||
}
|
||||
|
||||
pub fn product_component_total_cost(component: &ProductComponent) -> Currency {
|
||||
component.total_cost()
|
||||
}
|
||||
|
||||
// ProductComponentBuilder Functions
|
||||
pub fn product_component_builder_new() -> ProductComponentBuilder {
|
||||
ProductComponentBuilder::new()
|
||||
}
|
||||
|
||||
pub fn product_component_builder_id(builder: ProductComponentBuilder, id: i64) -> ProductComponentBuilder {
|
||||
builder.id(id as u32)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_name(builder: ProductComponentBuilder, name: &str) -> ProductComponentBuilder {
|
||||
builder.name(name)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_description(builder: ProductComponentBuilder, description: &str) -> ProductComponentBuilder {
|
||||
builder.description(description)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_quantity(builder: ProductComponentBuilder, quantity: i64) -> ProductComponentBuilder {
|
||||
builder.quantity(quantity as i32)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_energy_usage(builder: ProductComponentBuilder, energy_usage: f64) -> ProductComponentBuilder {
|
||||
builder.energy_usage(energy_usage)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_cost(builder: ProductComponentBuilder, cost: Currency) -> ProductComponentBuilder {
|
||||
builder.cost(cost)
|
||||
}
|
||||
|
||||
pub fn product_component_builder_build(builder: ProductComponentBuilder) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Product Functions
|
||||
pub fn product_type_product() -> ProductType {
|
||||
ProductType::Product
|
||||
}
|
||||
|
||||
pub fn product_type_service() -> ProductType {
|
||||
ProductType::Service
|
||||
}
|
||||
|
||||
pub fn product_status_active() -> ProductStatus {
|
||||
ProductStatus::Active
|
||||
}
|
||||
|
||||
pub fn product_status_error() -> ProductStatus {
|
||||
ProductStatus::Error
|
||||
}
|
||||
|
||||
pub fn product_status_end_of_life() -> ProductStatus {
|
||||
ProductStatus::EndOfLife
|
||||
}
|
||||
|
||||
pub fn product_status_paused() -> ProductStatus {
|
||||
ProductStatus::Paused
|
||||
}
|
||||
|
||||
pub fn product_status_available() -> ProductStatus {
|
||||
ProductStatus::Available
|
||||
}
|
||||
|
||||
pub fn product_status_unavailable() -> ProductStatus {
|
||||
ProductStatus::Unavailable
|
||||
}
|
||||
|
||||
pub fn product_add_component(product: &mut Product, component: ProductComponent) {
|
||||
product.add_component(component);
|
||||
}
|
||||
|
||||
pub fn product_set_purchase_period(product: &mut Product, purchase_till_days: i64) {
|
||||
let purchase_till = Utc::now() + Duration::days(purchase_till_days);
|
||||
product.set_purchase_period(purchase_till);
|
||||
}
|
||||
|
||||
pub fn product_set_active_period(product: &mut Product, active_till_days: i64) {
|
||||
let active_till = Utc::now() + Duration::days(active_till_days);
|
||||
product.set_active_period(active_till);
|
||||
}
|
||||
|
||||
pub fn product_is_purchasable(product: &Product) -> bool {
|
||||
product.is_purchasable()
|
||||
}
|
||||
|
||||
pub fn product_is_active(product: &Product) -> bool {
|
||||
product.is_active()
|
||||
}
|
||||
|
||||
pub fn product_cost_in_currency(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.cost_in_currency(currency_code) {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to calculate cost in {}", currency_code).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.cost_in_usd() {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to calculate cost in USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_total_energy_usage(product: &Product) -> f64 {
|
||||
product.total_energy_usage()
|
||||
}
|
||||
|
||||
pub fn product_components_cost(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.components_cost(currency_code) {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to calculate components cost in {}", currency_code).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn product_components_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match product.components_cost_in_usd() {
|
||||
Some(cost) => Ok(cost.to_rhai()),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Failed to calculate components cost in USD".into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// ProductBuilder Functions
|
||||
pub fn product_builder_new() -> ProductBuilder {
|
||||
ProductBuilder::new()
|
||||
}
|
||||
|
||||
pub fn product_builder_id(builder: ProductBuilder, id: i64) -> ProductBuilder {
|
||||
builder.id(id as u32)
|
||||
}
|
||||
|
||||
pub fn product_builder_name(builder: ProductBuilder, name: &str) -> ProductBuilder {
|
||||
builder.name(name)
|
||||
}
|
||||
|
||||
pub fn product_builder_description(builder: ProductBuilder, description: &str) -> ProductBuilder {
|
||||
builder.description(description)
|
||||
}
|
||||
|
||||
pub fn product_builder_price(builder: ProductBuilder, price: Currency) -> ProductBuilder {
|
||||
builder.price(price)
|
||||
}
|
||||
|
||||
pub fn product_builder_type(builder: ProductBuilder, type_: ProductType) -> ProductBuilder {
|
||||
builder.type_(type_)
|
||||
}
|
||||
|
||||
pub fn product_builder_category(builder: ProductBuilder, category: &str) -> ProductBuilder {
|
||||
builder.category(category)
|
||||
}
|
||||
|
||||
pub fn product_builder_status(builder: ProductBuilder, status: ProductStatus) -> ProductBuilder {
|
||||
builder.status(status)
|
||||
}
|
||||
|
||||
pub fn product_builder_max_amount(builder: ProductBuilder, max_amount: i64) -> ProductBuilder {
|
||||
builder.max_amount(max_amount as u16)
|
||||
}
|
||||
|
||||
pub fn product_builder_validity_days(builder: ProductBuilder, validity_days: i64) -> ProductBuilder {
|
||||
builder.validity_days(validity_days)
|
||||
}
|
||||
|
||||
pub fn product_builder_add_component(builder: ProductBuilder, component: ProductComponent) -> ProductBuilder {
|
||||
builder.add_component(component)
|
||||
}
|
||||
|
||||
pub fn product_builder_build(builder: ProductBuilder) -> Result<Product, Box<EvalAltResult>> {
|
||||
builder.build().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Exchange Rate Service Functions
|
||||
pub fn exchange_rate_convert(amount: f64, from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
|
||||
match EXCHANGE_RATE_SERVICE.convert(amount, from_currency, to_currency) {
|
||||
Some(converted) => Ok(converted),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert {} {} to {}", amount, from_currency, to_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exchange_rate_get_rate(from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
|
||||
match EXCHANGE_RATE_SERVICE.get_rate(from_currency, to_currency) {
|
||||
Some(rate) => Ok(rate),
|
||||
None => Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to get exchange rate from {} to {}", from_currency, to_currency).into(),
|
||||
Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
579
herodb_old/src/models/biz/sale.rs
Normal file
579
herodb_old/src/models/biz/sale.rs
Normal file
@@ -0,0 +1,579 @@
|
||||
use crate::db::{Model, Storable, DbError, DbResult, IndexKey};
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
use chrono::{DateTime, Utc};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
/// SaleStatus represents the status of a sale
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SaleStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// SaleItem represents an item in a sale
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SaleItem {
|
||||
pub id: u32,
|
||||
pub sale_id: u32,
|
||||
pub product_id: Option<u32>, // ID of the product sold (if this is a product sale)
|
||||
pub service_id: Option<u32>, // ID of the service sold (if this is a service sale)
|
||||
pub name: String,
|
||||
pub description: String, // Description of the item
|
||||
pub comments: String, // Additional comments about the item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
pub tax_amount: Currency, // Calculated tax amount
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
}
|
||||
|
||||
impl SaleItem {
|
||||
/// Create a new sale item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
sale_id: u32,
|
||||
product_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Validate that either product_id or service_id is provided, but not both
|
||||
assert!(
|
||||
(product_id.is_some() && service_id.is_none()) ||
|
||||
(product_id.is_none() && service_id.is_some()),
|
||||
"Either product_id or service_id must be provided, but not both"
|
||||
);
|
||||
// Calculate subtotal (before tax)
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_amount_value,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
sale_id,
|
||||
product_id,
|
||||
service_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total amount including tax
|
||||
pub fn total_with_tax(&self) -> Currency {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
self.subtotal.amount + self.tax_amount.amount,
|
||||
self.subtotal.currency_code.clone()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SaleItem
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleItemBuilder {
|
||||
id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl SaleItemBuilder {
|
||||
/// Create a new SaleItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
sale_id: None,
|
||||
product_id: None,
|
||||
service_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_id
|
||||
pub fn sale_id(mut self, sale_id: u32) -> Self {
|
||||
self.sale_id = Some(sale_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product_id
|
||||
pub fn product_id(mut self, product_id: Option<u32>) -> Self {
|
||||
// If setting product_id, ensure service_id is None
|
||||
if product_id.is_some() {
|
||||
self.service_id = None;
|
||||
}
|
||||
self.product_id = product_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: Option<u32>) -> Self {
|
||||
// If setting service_id, ensure product_id is None
|
||||
if service_id.is_some() {
|
||||
self.product_id = None;
|
||||
}
|
||||
self.service_id = service_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the unit_price
|
||||
pub fn unit_price(mut self, unit_price: Currency) -> Self {
|
||||
self.unit_price = Some(unit_price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SaleItem object
|
||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
|
||||
|
||||
// Validate that either product_id or service_id is provided, but not both
|
||||
if self.product_id.is_none() && self.service_id.is_none() {
|
||||
return Err("Either product_id or service_id must be provided");
|
||||
}
|
||||
if self.product_id.is_some() && self.service_id.is_some() {
|
||||
return Err("Only one of product_id or service_id can be provided");
|
||||
}
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_amount_value,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Ok(SaleItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
sale_id: self.sale_id.ok_or("sale_id is required")?,
|
||||
product_id: self.product_id,
|
||||
service_id: self.service_id,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sale represents a sale of products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Sale {
|
||||
pub id: u32,
|
||||
pub customer_id: u32, // ID of the customer making the purchase
|
||||
pub subtotal_amount: Currency, // Total before tax
|
||||
pub tax_amount: Currency, // Total tax
|
||||
pub total_amount: Currency, // Total including tax
|
||||
pub status: SaleStatus,
|
||||
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||
pub sale_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<SaleItem>,
|
||||
}
|
||||
|
||||
// Removed old Model trait implementation
|
||||
|
||||
impl Sale {
|
||||
/// Create a new sale with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
status: SaleStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_currency = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
subtotal_amount: zero_currency.clone(),
|
||||
tax_amount: zero_currency.clone(),
|
||||
total_amount: zero_currency,
|
||||
status,
|
||||
service_id: None,
|
||||
sale_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the sale and update the total amount
|
||||
pub fn add_item(&mut self, item: SaleItem) {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
||||
|
||||
// Update the amounts
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the amounts with the same currency
|
||||
self.subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
self.tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.tax_amount.amount,
|
||||
item.tax_amount.currency_code.clone()
|
||||
);
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount + item.tax_amount.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing totals
|
||||
// (Assumes all items have the same currency)
|
||||
self.subtotal_amount.amount += item.subtotal.amount;
|
||||
self.tax_amount.amount += item.tax_amount.amount;
|
||||
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
// Update the sale timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Recalculate all totals based on items
|
||||
pub fn recalculate_totals(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].subtotal.currency_code.clone();
|
||||
|
||||
// Calculate the totals
|
||||
let mut subtotal = 0.0;
|
||||
let mut tax_total = 0.0;
|
||||
|
||||
for item in &self.items {
|
||||
subtotal += item.subtotal.amount;
|
||||
tax_total += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Update the amounts
|
||||
self.subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal,
|
||||
currency_code.clone()
|
||||
);
|
||||
self.tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
tax_total,
|
||||
currency_code.clone()
|
||||
);
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal + tax_total,
|
||||
currency_code
|
||||
);
|
||||
|
||||
// Update the timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the sale
|
||||
pub fn update_status(&mut self, status: SaleStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Link this sale to an existing service
|
||||
pub fn link_to_service(&mut self, service_id: u32) {
|
||||
self.service_id = Some(service_id);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Sale
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleBuilder {
|
||||
id: Option<u32>,
|
||||
company_id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
buyer_name: Option<String>,
|
||||
buyer_email: Option<String>,
|
||||
subtotal_amount: Option<Currency>,
|
||||
tax_amount: Option<Currency>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<SaleStatus>,
|
||||
service_id: Option<u32>,
|
||||
sale_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<SaleItem>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl SaleBuilder {
|
||||
/// Create a new SaleBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
company_id: None,
|
||||
customer_id: None,
|
||||
buyer_name: None,
|
||||
buyer_email: None,
|
||||
subtotal_amount: None,
|
||||
tax_amount: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
service_id: None,
|
||||
sale_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the company_id
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = Some(company_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_name
|
||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||
self.buyer_name = Some(buyer_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_email
|
||||
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
|
||||
self.buyer_email = Some(buyer_email.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: SaleStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_date
|
||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||
self.sale_date = Some(sale_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the sale
|
||||
pub fn add_item(mut self, item: SaleItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Sale object
|
||||
pub fn build(self) -> Result<Sale, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty amounts
|
||||
let mut subtotal_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
let mut tax_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate amounts from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
if item.sale_id != id {
|
||||
return Err("Item sale_id must match sale id");
|
||||
}
|
||||
|
||||
subtotal_amount.amount += item.subtotal.amount;
|
||||
tax_amount.amount += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||
|
||||
Ok(Sale {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
|
||||
tax_amount: self.tax_amount.unwrap_or(tax_amount),
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_date: self.sale_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Sale {}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Sale {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"sale"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
||||
|
478
herodb_old/src/models/biz/service.rs
Normal file
478
herodb_old/src/models/biz/service.rs
Normal file
@@ -0,0 +1,478 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::{Model, Storable, DbError, DbResult, IndexKey}; // Import Model trait and IndexKey from db module
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// BillingFrequency represents the frequency of billing for a service
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BillingFrequency {
|
||||
Hourly,
|
||||
Daily,
|
||||
Weekly,
|
||||
Monthly,
|
||||
Yearly,
|
||||
}
|
||||
|
||||
/// ServiceStatus represents the status of a service
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ServiceStatus {
|
||||
Active,
|
||||
Paused,
|
||||
Cancelled,
|
||||
Completed,
|
||||
}
|
||||
|
||||
/// ServiceItem represents an item in a service
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceItem {
|
||||
pub id: u32,
|
||||
pub service_id: u32,
|
||||
pub product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the service item
|
||||
pub comments: String, // Additional comments about the service item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub active_till: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ServiceItem {
|
||||
/// Create a new service item
|
||||
pub fn new(
|
||||
id: u32,
|
||||
service_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
Self {
|
||||
id,
|
||||
service_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the subtotal based on quantity and unit price
|
||||
pub fn calculate_subtotal(&mut self) {
|
||||
let amount = self.unit_price.amount * self.quantity as f64;
|
||||
self.subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
self.unit_price.currency_code.clone()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Builder for ServiceItem
|
||||
pub struct ServiceItemBuilder {
|
||||
id: Option<u32>,
|
||||
service_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
is_taxable: Option<bool>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl ServiceItemBuilder {
|
||||
/// Create a new ServiceItemBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
service_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
is_taxable: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the product_id
|
||||
pub fn product_id(mut self, product_id: u32) -> Self {
|
||||
self.product_id = Some(product_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the unit_price
|
||||
pub fn unit_price(mut self, unit_price: Currency) -> Self {
|
||||
self.unit_price = Some(unit_price);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set is_taxable
|
||||
pub fn is_taxable(mut self, is_taxable: bool) -> Self {
|
||||
self.is_taxable = Some(is_taxable);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the ServiceItem object
|
||||
pub fn build(self) -> Result<ServiceItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0);
|
||||
let is_taxable = self.is_taxable.unwrap_or(false);
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
amount,
|
||||
unit_price.currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate tax amount if taxable
|
||||
let tax_amount = if is_taxable {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
subtotal.amount * tax_rate,
|
||||
unit_price.currency_code.clone()
|
||||
)
|
||||
} else {
|
||||
Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
unit_price.currency_code.clone()
|
||||
)
|
||||
};
|
||||
|
||||
Ok(ServiceItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
service_id: self.service_id.ok_or("service_id is required")?,
|
||||
product_id: self.product_id.ok_or("product_id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Service represents a recurring service with billing frequency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
pub id: u32,
|
||||
pub customer_id: u32,
|
||||
pub total_amount: Currency,
|
||||
pub status: ServiceStatus,
|
||||
pub billing_frequency: BillingFrequency,
|
||||
pub service_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub items: Vec<ServiceItem>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Create a new service with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
customer_id: u32,
|
||||
currency_code: String,
|
||||
status: ServiceStatus,
|
||||
billing_frequency: BillingFrequency,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
customer_id,
|
||||
total_amount: Currency::new(0, 0.0, currency_code),
|
||||
status,
|
||||
billing_frequency,
|
||||
service_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an item to the service and update the total amount
|
||||
pub fn add_item(&mut self, item: ServiceItem) {
|
||||
// Make sure the item's service_id matches this service
|
||||
assert_eq!(self.id, item.service_id, "Item service_id must match service id");
|
||||
|
||||
// Update the total amount
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount ,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
// Update the service timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Calculate the total amount based on all items
|
||||
pub fn calculate_total(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].subtotal.currency_code.clone();
|
||||
|
||||
// Calculate the total amount
|
||||
let mut total = 0.0;
|
||||
for item in &self.items {
|
||||
total += item.subtotal.amount;
|
||||
}
|
||||
|
||||
// Update the total amount
|
||||
self.total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
total,
|
||||
currency_code
|
||||
);
|
||||
|
||||
// Update the service timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the service
|
||||
pub fn update_status(&mut self, status: ServiceStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Service
|
||||
pub struct ServiceBuilder {
|
||||
id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<ServiceStatus>,
|
||||
billing_frequency: Option<BillingFrequency>,
|
||||
service_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
items: Vec<ServiceItem>,
|
||||
currency_code: Option<String>,
|
||||
}
|
||||
|
||||
impl ServiceBuilder {
|
||||
/// Create a new ServiceBuilder with all fields set to None
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
customer_id: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
billing_frequency: None,
|
||||
service_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
items: Vec::new(),
|
||||
currency_code: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the currency_code
|
||||
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
|
||||
self.currency_code = Some(currency_code.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the status
|
||||
pub fn status(mut self, status: ServiceStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the billing_frequency
|
||||
pub fn billing_frequency(mut self, billing_frequency: BillingFrequency) -> Self {
|
||||
self.billing_frequency = Some(billing_frequency);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_date
|
||||
pub fn service_date(mut self, service_date: DateTime<Utc>) -> Self {
|
||||
self.service_date = Some(service_date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an item to the service
|
||||
pub fn add_item(mut self, item: ServiceItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Service object
|
||||
pub fn build(self) -> Result<Service, &'static str> {
|
||||
let now = Utc::now();
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount
|
||||
let mut total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
0.0,
|
||||
currency_code.clone()
|
||||
);
|
||||
|
||||
// Calculate total amount from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's service_id matches this service
|
||||
if item.service_id != id {
|
||||
return Err("Item service_id must match service id");
|
||||
}
|
||||
|
||||
if total_amount.amount == 0.0 {
|
||||
// First item, initialize the total amount with the same currency
|
||||
total_amount = Currency::new(
|
||||
0, // Use 0 as a temporary ID
|
||||
item.subtotal.amount,
|
||||
item.subtotal.currency_code.clone()
|
||||
);
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
total_amount.amount += item.subtotal.amount ;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Service {
|
||||
id,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
billing_frequency: self.billing_frequency.ok_or("billing_frequency is required")?,
|
||||
service_date: self.service_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
items: self.items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Storable trait
|
||||
impl Storable for Service {
|
||||
}
|
||||
|
||||
// Implement Model trait
|
||||
impl Model for Service {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"service"
|
||||
}
|
||||
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Add an index for customer_id
|
||||
keys.push(IndexKey {
|
||||
name: "customer_id",
|
||||
value: self.customer_id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
keys
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user