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, } 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, ) -> 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, service_id: Option, product_id: Option, name: Option, quantity: Option, unit_price: Option, subtotal: Option, tax_rate: Option, tax_amount: Option, is_taxable: Option, active_till: Option>, } 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>(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) -> Self { self.active_till = Some(active_till); self } /// Build the ServiceItem object pub fn build(self) -> Result { 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, pub created_at: DateTime, pub updated_at: DateTime, pub items: Vec, } 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, customer_id: Option, total_amount: Option, status: Option, billing_frequency: Option, service_date: Option>, created_at: Option>, updated_at: Option>, items: Vec, currency_code: Option, } 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>(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) -> 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 { 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" } }