This commit is contained in:
kristof 2025-04-04 12:43:20 +02:00
parent be3ad84c7d
commit 8759159925
7 changed files with 1772 additions and 3 deletions

View File

@ -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);

View 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

View 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"
}
}

View 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"
}
}

View 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"
}
}

View File

@ -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};

View 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"
}
}