...
This commit is contained in:
parent
be3ad84c7d
commit
8759159925
@ -1,7 +1,7 @@
|
||||
use crate::db::db::DB;
|
||||
use crate::db::base::{SledDBResult, SledModel};
|
||||
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
|
||||
impl_model_methods!(Product, product, products);
|
||||
@ -13,4 +13,16 @@ impl_model_methods!(Sale, sale, sales);
|
||||
impl_model_methods!(Currency, currency, currencies);
|
||||
|
||||
// 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 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 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