469 lines
14 KiB
Rust
469 lines
14 KiB
Rust
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"
|
|
}
|
|
} |