...
This commit is contained in:
parent
be3ad84c7d
commit
8759159925
@ -1,7 +1,7 @@
|
|||||||
use crate::db::db::DB;
|
use crate::db::db::DB;
|
||||||
use crate::db::base::{SledDBResult, SledModel};
|
use crate::db::base::{SledDBResult, SledModel};
|
||||||
use crate::impl_model_methods;
|
use crate::impl_model_methods;
|
||||||
use crate::models::biz::{Product, Sale, Currency, ExchangeRate};
|
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
|
||||||
|
|
||||||
// Implement model-specific methods for Product
|
// Implement model-specific methods for Product
|
||||||
impl_model_methods!(Product, product, products);
|
impl_model_methods!(Product, product, products);
|
||||||
@ -13,4 +13,16 @@ impl_model_methods!(Sale, sale, sales);
|
|||||||
impl_model_methods!(Currency, currency, currencies);
|
impl_model_methods!(Currency, currency, currencies);
|
||||||
|
|
||||||
// Implement model-specific methods for ExchangeRate
|
// Implement model-specific methods for ExchangeRate
|
||||||
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
|
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Service
|
||||||
|
impl_model_methods!(Service, service, services);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Customer
|
||||||
|
impl_model_methods!(Customer, customer, customers);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Contract
|
||||||
|
impl_model_methods!(Contract, contract, contracts);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Invoice
|
||||||
|
impl_model_methods!(Invoice, invoice, invoices);
|
371
herodb/src/models/biz/business_models_plan.md
Normal file
371
herodb/src/models/biz/business_models_plan.md
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
# Business Models Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the plan for implementing new business models in the codebase:
|
||||||
|
|
||||||
|
1. **Service**: For tracking recurring payments (similar to Sale)
|
||||||
|
2. **Customer**: For storing customer information
|
||||||
|
3. **Contract**: For linking services or sales to customers
|
||||||
|
4. **Invoice**: For invoicing customers
|
||||||
|
|
||||||
|
## Model Diagrams
|
||||||
|
|
||||||
|
### Core Models and Relationships
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Service {
|
||||||
|
+id: u32
|
||||||
|
+customer_id: u32
|
||||||
|
+total_amount: Currency
|
||||||
|
+status: ServiceStatus
|
||||||
|
+billing_frequency: BillingFrequency
|
||||||
|
+service_date: DateTime~Utc~
|
||||||
|
+created_at: DateTime~Utc~
|
||||||
|
+updated_at: DateTime~Utc~
|
||||||
|
+items: Vec~ServiceItem~
|
||||||
|
+calculate_total()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceItem {
|
||||||
|
+id: u32
|
||||||
|
+service_id: u32
|
||||||
|
+name: String
|
||||||
|
+quantity: i32
|
||||||
|
+unit_price: Currency
|
||||||
|
+subtotal: Currency
|
||||||
|
+tax_rate: f64
|
||||||
|
+tax_amount: Currency
|
||||||
|
+is_taxable: bool
|
||||||
|
+active_till: DateTime~Utc~
|
||||||
|
}
|
||||||
|
|
||||||
|
class Customer {
|
||||||
|
+id: u32
|
||||||
|
+name: String
|
||||||
|
+description: String
|
||||||
|
+pubkey: String
|
||||||
|
+contact_ids: Vec~u32~
|
||||||
|
+created_at: DateTime~Utc~
|
||||||
|
+updated_at: DateTime~Utc~
|
||||||
|
}
|
||||||
|
|
||||||
|
class Contract {
|
||||||
|
+id: u32
|
||||||
|
+customer_id: u32
|
||||||
|
+service_id: Option~u32~
|
||||||
|
+sale_id: Option~u32~
|
||||||
|
+terms: String
|
||||||
|
+start_date: DateTime~Utc~
|
||||||
|
+end_date: DateTime~Utc~
|
||||||
|
+auto_renewal: bool
|
||||||
|
+renewal_terms: String
|
||||||
|
+status: ContractStatus
|
||||||
|
+created_at: DateTime~Utc~
|
||||||
|
+updated_at: DateTime~Utc~
|
||||||
|
}
|
||||||
|
|
||||||
|
class Invoice {
|
||||||
|
+id: u32
|
||||||
|
+customer_id: u32
|
||||||
|
+total_amount: Currency
|
||||||
|
+balance_due: Currency
|
||||||
|
+status: InvoiceStatus
|
||||||
|
+payment_status: PaymentStatus
|
||||||
|
+issue_date: DateTime~Utc~
|
||||||
|
+due_date: DateTime~Utc~
|
||||||
|
+created_at: DateTime~Utc~
|
||||||
|
+updated_at: DateTime~Utc~
|
||||||
|
+items: Vec~InvoiceItem~
|
||||||
|
+payments: Vec~Payment~
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceItem {
|
||||||
|
+id: u32
|
||||||
|
+invoice_id: u32
|
||||||
|
+description: String
|
||||||
|
+amount: Currency
|
||||||
|
+service_id: Option~u32~
|
||||||
|
+sale_id: Option~u32~
|
||||||
|
}
|
||||||
|
|
||||||
|
class Payment {
|
||||||
|
+amount: Currency
|
||||||
|
+date: DateTime~Utc~
|
||||||
|
+method: String
|
||||||
|
}
|
||||||
|
|
||||||
|
Service "1" -- "many" ServiceItem : contains
|
||||||
|
Customer "1" -- "many" Service : has
|
||||||
|
Customer "1" -- "many" Contract : has
|
||||||
|
Contract "1" -- "0..1" Service : references
|
||||||
|
Contract "1" -- "0..1" Sale : references
|
||||||
|
Invoice "1" -- "many" InvoiceItem : contains
|
||||||
|
Invoice "1" -- "many" Payment : contains
|
||||||
|
Customer "1" -- "many" Invoice : has
|
||||||
|
InvoiceItem "1" -- "0..1" Service : references
|
||||||
|
InvoiceItem "1" -- "0..1" Sale : references
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enums and Supporting Types
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class BillingFrequency {
|
||||||
|
<<enumeration>>
|
||||||
|
Hourly
|
||||||
|
Daily
|
||||||
|
Weekly
|
||||||
|
Monthly
|
||||||
|
Yearly
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceStatus {
|
||||||
|
<<enumeration>>
|
||||||
|
Active
|
||||||
|
Paused
|
||||||
|
Cancelled
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContractStatus {
|
||||||
|
<<enumeration>>
|
||||||
|
Active
|
||||||
|
Expired
|
||||||
|
Terminated
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceStatus {
|
||||||
|
<<enumeration>>
|
||||||
|
Draft
|
||||||
|
Sent
|
||||||
|
Paid
|
||||||
|
Overdue
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentStatus {
|
||||||
|
<<enumeration>>
|
||||||
|
Unpaid
|
||||||
|
PartiallyPaid
|
||||||
|
Paid
|
||||||
|
}
|
||||||
|
|
||||||
|
Service -- ServiceStatus : has
|
||||||
|
Service -- BillingFrequency : has
|
||||||
|
Contract -- ContractStatus : has
|
||||||
|
Invoice -- InvoiceStatus : has
|
||||||
|
Invoice -- PaymentStatus : has
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Implementation Plan
|
||||||
|
|
||||||
|
### 1. Service and ServiceItem (service.rs)
|
||||||
|
|
||||||
|
The Service model will be similar to Sale but designed for recurring payments:
|
||||||
|
|
||||||
|
- **Service**: Main struct for tracking recurring services
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- customer_id: u32
|
||||||
|
- total_amount: Currency
|
||||||
|
- status: ServiceStatus
|
||||||
|
- billing_frequency: BillingFrequency
|
||||||
|
- service_date: DateTime<Utc>
|
||||||
|
- created_at: DateTime<Utc>
|
||||||
|
- updated_at: DateTime<Utc>
|
||||||
|
- items: Vec<ServiceItem>
|
||||||
|
- Methods:
|
||||||
|
- calculate_total(): Updates the total_amount based on all items
|
||||||
|
- add_item(item: ServiceItem): Adds an item and updates the total
|
||||||
|
- update_status(status: ServiceStatus): Updates the status and timestamp
|
||||||
|
|
||||||
|
- **ServiceItem**: Items within a service (similar to SaleItem)
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- service_id: u32
|
||||||
|
- name: String
|
||||||
|
- quantity: i32
|
||||||
|
- unit_price: Currency
|
||||||
|
- subtotal: Currency
|
||||||
|
- tax_rate: f64
|
||||||
|
- tax_amount: Currency
|
||||||
|
- is_taxable: bool
|
||||||
|
- active_till: DateTime<Utc>
|
||||||
|
- Methods:
|
||||||
|
- calculate_subtotal(): Calculates subtotal based on quantity and unit_price
|
||||||
|
- calculate_tax(): Calculates tax amount based on subtotal and tax_rate
|
||||||
|
|
||||||
|
- **BillingFrequency**: Enum for different billing periods
|
||||||
|
- Variants: Hourly, Daily, Weekly, Monthly, Yearly
|
||||||
|
|
||||||
|
- **ServiceStatus**: Enum for service status
|
||||||
|
- Variants: Active, Paused, Cancelled, Completed
|
||||||
|
|
||||||
|
### 2. Customer (customer.rs)
|
||||||
|
|
||||||
|
The Customer model will store customer information:
|
||||||
|
|
||||||
|
- **Customer**: Main struct for customer data
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- name: String
|
||||||
|
- description: String
|
||||||
|
- pubkey: String
|
||||||
|
- contact_ids: Vec<u32>
|
||||||
|
- created_at: DateTime<Utc>
|
||||||
|
- updated_at: DateTime<Utc>
|
||||||
|
- Methods:
|
||||||
|
- add_contact(contact_id: u32): Adds a contact ID to the list
|
||||||
|
- remove_contact(contact_id: u32): Removes a contact ID from the list
|
||||||
|
|
||||||
|
### 3. Contract (contract.rs)
|
||||||
|
|
||||||
|
The Contract model will link services or sales to customers:
|
||||||
|
|
||||||
|
- **Contract**: Main struct for contract data
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- customer_id: u32
|
||||||
|
- service_id: Option<u32>
|
||||||
|
- sale_id: Option<u32>
|
||||||
|
- terms: String
|
||||||
|
- start_date: DateTime<Utc>
|
||||||
|
- end_date: DateTime<Utc>
|
||||||
|
- auto_renewal: bool
|
||||||
|
- renewal_terms: String
|
||||||
|
- status: ContractStatus
|
||||||
|
- created_at: DateTime<Utc>
|
||||||
|
- updated_at: DateTime<Utc>
|
||||||
|
- Methods:
|
||||||
|
- is_active(): bool - Checks if the contract is currently active
|
||||||
|
- is_expired(): bool - Checks if the contract has expired
|
||||||
|
- renew(): Updates the contract dates based on renewal terms
|
||||||
|
|
||||||
|
- **ContractStatus**: Enum for contract status
|
||||||
|
- Variants: Active, Expired, Terminated
|
||||||
|
|
||||||
|
### 4. Invoice (invoice.rs)
|
||||||
|
|
||||||
|
The Invoice model will handle billing:
|
||||||
|
|
||||||
|
- **Invoice**: Main struct for invoice data
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- customer_id: u32
|
||||||
|
- total_amount: Currency
|
||||||
|
- balance_due: Currency
|
||||||
|
- status: InvoiceStatus
|
||||||
|
- payment_status: PaymentStatus
|
||||||
|
- issue_date: DateTime<Utc>
|
||||||
|
- due_date: DateTime<Utc>
|
||||||
|
- created_at: DateTime<Utc>
|
||||||
|
- updated_at: DateTime<Utc>
|
||||||
|
- items: Vec<InvoiceItem>
|
||||||
|
- payments: Vec<Payment>
|
||||||
|
- Methods:
|
||||||
|
- calculate_total(): Updates the total_amount based on all items
|
||||||
|
- add_item(item: InvoiceItem): Adds an item and updates the total
|
||||||
|
- add_payment(payment: Payment): Adds a payment and updates balance_due and payment_status
|
||||||
|
- update_status(status: InvoiceStatus): Updates the status and timestamp
|
||||||
|
- calculate_balance(): Updates the balance_due based on total_amount and payments
|
||||||
|
|
||||||
|
- **InvoiceItem**: Items within an invoice
|
||||||
|
- Fields:
|
||||||
|
- id: u32
|
||||||
|
- invoice_id: u32
|
||||||
|
- description: String
|
||||||
|
- amount: Currency
|
||||||
|
- service_id: Option<u32>
|
||||||
|
- sale_id: Option<u32>
|
||||||
|
|
||||||
|
- **Payment**: Struct for tracking payments
|
||||||
|
- Fields:
|
||||||
|
- amount: Currency
|
||||||
|
- date: DateTime<Utc>
|
||||||
|
- method: String
|
||||||
|
|
||||||
|
- **InvoiceStatus**: Enum for invoice status
|
||||||
|
- Variants: Draft, Sent, Paid, Overdue, Cancelled
|
||||||
|
|
||||||
|
- **PaymentStatus**: Enum for payment status
|
||||||
|
- Variants: Unpaid, PartiallyPaid, Paid
|
||||||
|
|
||||||
|
### 5. Updates to mod.rs
|
||||||
|
|
||||||
|
We'll need to update the mod.rs file to include the new modules and re-export the types:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Updates to model_methods.rs
|
||||||
|
|
||||||
|
We'll need to update the model_methods.rs file to implement the model methods for the new models:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::db::db::DB;
|
||||||
|
use crate::db::base::{SledDBResult, SledModel};
|
||||||
|
use crate::impl_model_methods;
|
||||||
|
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
|
||||||
|
|
||||||
|
// Implement model-specific methods for Product
|
||||||
|
impl_model_methods!(Product, product, products);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Sale
|
||||||
|
impl_model_methods!(Sale, sale, sales);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Currency
|
||||||
|
impl_model_methods!(Currency, currency, currencies);
|
||||||
|
|
||||||
|
// Implement model-specific methods for ExchangeRate
|
||||||
|
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Service
|
||||||
|
impl_model_methods!(Service, service, services);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Customer
|
||||||
|
impl_model_methods!(Customer, customer, customers);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Contract
|
||||||
|
impl_model_methods!(Contract, contract, contracts);
|
||||||
|
|
||||||
|
// Implement model-specific methods for Invoice
|
||||||
|
impl_model_methods!(Invoice, invoice, invoices);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Approach
|
||||||
|
|
||||||
|
1. Create the new model files (service.rs, customer.rs, contract.rs, invoice.rs)
|
||||||
|
2. Implement the structs, enums, and methods for each model
|
||||||
|
3. Update mod.rs to include the new modules and re-export the types
|
||||||
|
4. Update model_methods.rs to implement the model methods for the new models
|
||||||
|
5. Test the new models with example code
|
250
herodb/src/models/biz/contract.rs
Normal file
250
herodb/src/models/biz/contract.rs
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
use crate::db::base::{SledModel, Storable}; // Import Sled traits 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Contract {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Contract {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"contract"
|
||||||
|
}
|
||||||
|
}
|
148
herodb/src/models/biz/customer.rs
Normal file
148
herodb/src/models/biz/customer.rs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
use crate::db::base::{SledModel, Storable}; // Import Sled traits 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Customer {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Customer {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"customer"
|
||||||
|
}
|
||||||
|
}
|
507
herodb/src/models/biz/invoice.rs
Normal file
507
herodb/src/models/biz/invoice.rs
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||||
|
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Payment {
|
||||||
|
/// Create a new payment
|
||||||
|
pub fn new(amount: Currency, method: String) -> Self {
|
||||||
|
Self {
|
||||||
|
amount,
|
||||||
|
date: Utc::now(),
|
||||||
|
method,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: 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 {
|
||||||
|
amount: item.amount.amount,
|
||||||
|
currency_code: item.amount.currency_code.clone(),
|
||||||
|
};
|
||||||
|
self.balance_due = Currency {
|
||||||
|
amount: item.amount.amount,
|
||||||
|
currency_code: 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 {
|
||||||
|
amount: total,
|
||||||
|
currency_code: 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 {
|
||||||
|
amount: balance,
|
||||||
|
currency_code: 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 {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement Storable trait (provides default dump/load)
|
||||||
|
impl Storable for Invoice {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Invoice {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"invoice"
|
||||||
|
}
|
||||||
|
}
|
@ -2,15 +2,27 @@ pub mod currency;
|
|||||||
pub mod product;
|
pub mod product;
|
||||||
pub mod sale;
|
pub mod sale;
|
||||||
pub mod exchange_rate;
|
pub mod exchange_rate;
|
||||||
|
pub mod service;
|
||||||
|
pub mod customer;
|
||||||
|
pub mod contract;
|
||||||
|
pub mod invoice;
|
||||||
|
|
||||||
// Re-export all model types for convenience
|
// Re-export all model types for convenience
|
||||||
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
|
||||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||||
pub use currency::Currency;
|
pub use currency::Currency;
|
||||||
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
|
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
|
// Re-export builder types
|
||||||
pub use product::{ProductBuilder, ProductComponentBuilder};
|
pub use product::{ProductBuilder, ProductComponentBuilder};
|
||||||
pub use sale::{SaleBuilder, SaleItemBuilder};
|
pub use sale::{SaleBuilder, SaleItemBuilder};
|
||||||
pub use currency::CurrencyBuilder;
|
pub use currency::CurrencyBuilder;
|
||||||
pub use exchange_rate::ExchangeRateBuilder;
|
pub use exchange_rate::ExchangeRateBuilder;
|
||||||
|
pub use service::{ServiceBuilder, ServiceItemBuilder};
|
||||||
|
pub use customer::CustomerBuilder;
|
||||||
|
pub use contract::ContractBuilder;
|
||||||
|
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};
|
469
herodb/src/models/biz/service.rs
Normal file
469
herodb/src/models/biz/service.rs
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||||
|
use crate::db::base::{SledModel, Storable}; // Import Sled traits 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 quantity: i32,
|
||||||
|
pub unit_price: Currency,
|
||||||
|
pub subtotal: Currency,
|
||||||
|
pub tax_rate: f64,
|
||||||
|
pub tax_amount: Currency,
|
||||||
|
pub is_taxable: bool,
|
||||||
|
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,
|
||||||
|
quantity: i32,
|
||||||
|
unit_price: Currency,
|
||||||
|
tax_rate: f64,
|
||||||
|
is_taxable: bool,
|
||||||
|
active_till: DateTime<Utc>,
|
||||||
|
) -> Self {
|
||||||
|
// Calculate subtotal
|
||||||
|
let amount = unit_price.amount * quantity as f64;
|
||||||
|
let subtotal = Currency {
|
||||||
|
amount,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate tax amount if taxable
|
||||||
|
let tax_amount = if is_taxable {
|
||||||
|
Currency {
|
||||||
|
amount: subtotal.amount * tax_rate,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Currency {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
service_id,
|
||||||
|
product_id,
|
||||||
|
name,
|
||||||
|
quantity,
|
||||||
|
unit_price,
|
||||||
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
|
is_taxable,
|
||||||
|
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 {
|
||||||
|
amount,
|
||||||
|
currency_code: self.unit_price.currency_code.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the tax amount based on subtotal and tax rate
|
||||||
|
pub fn calculate_tax(&mut self) {
|
||||||
|
if self.is_taxable {
|
||||||
|
self.tax_amount = Currency {
|
||||||
|
amount: self.subtotal.amount * self.tax_rate,
|
||||||
|
currency_code: self.subtotal.currency_code.clone(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.tax_amount = Currency {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: self.subtotal.currency_code.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for ServiceItem
|
||||||
|
pub struct ServiceItemBuilder {
|
||||||
|
id: Option<u32>,
|
||||||
|
service_id: Option<u32>,
|
||||||
|
product_id: Option<u32>,
|
||||||
|
name: 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,
|
||||||
|
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 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 {
|
||||||
|
amount,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate tax amount if taxable
|
||||||
|
let tax_amount = if is_taxable {
|
||||||
|
Currency {
|
||||||
|
amount: subtotal.amount * tax_rate,
|
||||||
|
currency_code: unit_price.currency_code.clone(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Currency {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: 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")?,
|
||||||
|
quantity,
|
||||||
|
unit_price,
|
||||||
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
|
is_taxable,
|
||||||
|
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 { amount: 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 {
|
||||||
|
amount: item.subtotal.amount + item.tax_amount.amount,
|
||||||
|
currency_code: 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 + item.tax_amount.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 + item.tax_amount.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the total amount
|
||||||
|
self.total_amount = Currency {
|
||||||
|
amount: 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 {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: 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 {
|
||||||
|
amount: item.subtotal.amount + item.tax_amount.amount,
|
||||||
|
currency_code: item.subtotal.currency_code.clone(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Add to the existing total
|
||||||
|
// (Assumes all items have the same currency)
|
||||||
|
total_amount.amount += item.subtotal.amount + item.tax_amount.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 (provides default dump/load)
|
||||||
|
impl Storable for Service {}
|
||||||
|
|
||||||
|
// Implement SledModel trait
|
||||||
|
impl SledModel for Service {
|
||||||
|
fn get_id(&self) -> String {
|
||||||
|
self.id.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn db_prefix() -> &'static str {
|
||||||
|
"service"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user