use crate::db::base::{SledModel, Storable}; use crate::models::biz::Currency; // Use crate:: for importing from the module // Import Sled traits from db module // use super::db::Model; // Removed old Model trait import use chrono::{DateTime, Utc}; use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; // use std::collections::HashMap; // Removed unused import /// SaleStatus represents the status of a sale #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SaleStatus { Pending, Completed, Cancelled, } /// SaleItem represents an item in a sale #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SaleItem { pub id: u32, pub sale_id: u32, pub product_id: u32, pub name: String, pub description: String, // Description of the item pub comments: String, // Additional comments about the item pub quantity: i32, pub unit_price: Currency, pub subtotal: Currency, pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%) pub tax_amount: Currency, // Calculated tax amount pub active_till: DateTime, // after this product no longer active if e.g. a service } impl SaleItem { /// Create a new sale item pub fn new( id: u32, sale_id: u32, product_id: u32, name: String, description: String, comments: String, quantity: i32, unit_price: Currency, tax_rate: f64, active_till: DateTime, ) -> Self { // Calculate subtotal (before tax) let amount = unit_price.amount * quantity as f64; let subtotal = Currency { amount, currency_code: unit_price.currency_code.clone(), }; // Calculate tax amount let tax_amount_value = subtotal.amount * (tax_rate / 100.0); let tax_amount = Currency { amount: tax_amount_value, currency_code: unit_price.currency_code.clone(), }; Self { id, sale_id, product_id, name, description, comments, quantity, unit_price, subtotal, tax_rate, tax_amount, active_till, } } /// Get the total amount including tax pub fn total_with_tax(&self) -> Currency { Currency { amount: self.subtotal.amount + self.tax_amount.amount, currency_code: self.subtotal.currency_code.clone(), } } } /// Builder for SaleItem #[derive(Clone, CustomType)] pub struct SaleItemBuilder { id: Option, sale_id: Option, product_id: Option, name: Option, description: Option, comments: Option, quantity: Option, unit_price: Option, subtotal: Option, tax_rate: Option, tax_amount: Option, active_till: Option>, } impl SaleItemBuilder { /// Create a new SaleItemBuilder with all fields set to None pub fn new() -> Self { Self { id: None, sale_id: None, product_id: None, name: None, description: None, comments: None, quantity: None, unit_price: None, subtotal: None, tax_rate: None, tax_amount: None, active_till: None, } } /// Set the id pub fn id(mut self, id: u32) -> Self { self.id = Some(id); self } /// Set the sale_id pub fn sale_id(mut self, sale_id: u32) -> Self { self.sale_id = Some(sale_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 description pub fn description>(mut self, description: S) -> Self { self.description = Some(description.into()); self } /// Set the comments pub fn comments>(mut self, comments: S) -> Self { self.comments = Some(comments.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 the active_till pub fn active_till(mut self, active_till: DateTime) -> Self { self.active_till = Some(active_till); self } /// Build the SaleItem 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); // Default to 0% tax if not specified // Calculate subtotal let amount = unit_price.amount * quantity as f64; let subtotal = Currency { amount, currency_code: unit_price.currency_code.clone(), }; // Calculate tax amount let tax_amount_value = subtotal.amount * (tax_rate / 100.0); let tax_amount = Currency { amount: tax_amount_value, currency_code: unit_price.currency_code.clone(), }; Ok(SaleItem { id: self.id.ok_or("id is required")?, sale_id: self.sale_id.ok_or("sale_id is required")?, product_id: self.product_id.ok_or("product_id is required")?, name: self.name.ok_or("name is required")?, description: self.description.unwrap_or_default(), comments: self.comments.unwrap_or_default(), quantity, unit_price, subtotal, tax_rate, tax_amount, active_till: self.active_till.ok_or("active_till is required")?, }) } } /// Sale represents a sale of products or services #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Sale { pub id: u32, pub company_id: u32, pub customer_id: u32, // ID of the customer making the purchase pub buyer_name: String, pub buyer_email: String, pub subtotal_amount: Currency, // Total before tax pub tax_amount: Currency, // Total tax pub total_amount: Currency, // Total including tax pub status: SaleStatus, pub service_id: Option, // ID of the service created from this sale (if applicable) pub sale_date: DateTime, pub created_at: DateTime, pub updated_at: DateTime, pub items: Vec, } // Removed old Model trait implementation impl Sale { /// Create a new sale with default timestamps pub fn new( id: u32, company_id: u32, customer_id: u32, buyer_name: String, buyer_email: String, currency_code: String, status: SaleStatus, ) -> Self { let now = Utc::now(); let zero_currency = Currency { amount: 0.0, currency_code: currency_code.clone(), }; Self { id, company_id, customer_id, buyer_name, buyer_email, subtotal_amount: zero_currency.clone(), tax_amount: zero_currency.clone(), total_amount: zero_currency, status, service_id: None, sale_date: now, created_at: now, updated_at: now, items: Vec::new(), } } /// Add an item to the sale and update the total amount pub fn add_item(&mut self, item: SaleItem) { // Make sure the item's sale_id matches this sale assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id"); // Update the amounts if self.items.is_empty() { // First item, initialize the amounts with the same currency self.subtotal_amount = Currency { amount: item.subtotal.amount, currency_code: item.subtotal.currency_code.clone(), }; self.tax_amount = Currency { amount: item.tax_amount.amount, currency_code: item.tax_amount.currency_code.clone(), }; self.total_amount = Currency { amount: item.subtotal.amount + item.tax_amount.amount, currency_code: item.subtotal.currency_code.clone(), }; } else { // Add to the existing totals // (Assumes all items have the same currency) self.subtotal_amount.amount += item.subtotal.amount; self.tax_amount.amount += item.tax_amount.amount; self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount; } // Add the item to the list self.items.push(item); // Update the sale timestamp self.updated_at = Utc::now(); } /// Recalculate all totals based on items pub fn recalculate_totals(&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 totals let mut subtotal = 0.0; let mut tax_total = 0.0; for item in &self.items { subtotal += item.subtotal.amount; tax_total += item.tax_amount.amount; } // Update the amounts self.subtotal_amount = Currency { amount: subtotal, currency_code: currency_code.clone(), }; self.tax_amount = Currency { amount: tax_total, currency_code: currency_code.clone(), }; self.total_amount = Currency { amount: subtotal + tax_total, currency_code, }; // Update the timestamp self.updated_at = Utc::now(); } /// Update the status of the sale pub fn update_status(&mut self, status: SaleStatus) { self.status = status; self.updated_at = Utc::now(); } /// Create a service from this sale /// This method should be called when a product of type Service is sold pub fn create_service(&mut self, service_id: u32, status: crate::models::biz::ServiceStatus, billing_frequency: crate::models::biz::BillingFrequency) -> Result { use crate::models::biz::{Service, ServiceItem, ServiceStatus, BillingFrequency}; // Create a new service let mut service = Service::new( service_id, self.customer_id, self.total_amount.currency_code.clone(), status, billing_frequency, ); // Convert sale items to service items for sale_item in &self.items { // Check if the product is a service type // In a real implementation, you would check the product type from the database // Create a service item from the sale item let service_item = ServiceItem::new( sale_item.id, service_id, sale_item.product_id, sale_item.name.clone(), sale_item.description.clone(), // Copy description from sale item sale_item.comments.clone(), // Copy comments from sale item sale_item.quantity, sale_item.unit_price.clone(), sale_item.tax_rate, true, // is_taxable sale_item.active_till, ); // Add the service item to the service service.add_item(service_item); } // Link this sale to the service self.service_id = Some(service_id); self.updated_at = Utc::now(); Ok(service) } } /// Builder for Sale #[derive(Clone, CustomType)] pub struct SaleBuilder { id: Option, company_id: Option, customer_id: Option, buyer_name: Option, buyer_email: Option, subtotal_amount: Option, tax_amount: Option, total_amount: Option, status: Option, service_id: Option, sale_date: Option>, created_at: Option>, updated_at: Option>, items: Vec, currency_code: Option, } impl SaleBuilder { /// Create a new SaleBuilder with all fields set to None pub fn new() -> Self { Self { id: None, company_id: None, customer_id: None, buyer_name: None, buyer_email: None, subtotal_amount: None, tax_amount: None, total_amount: None, status: None, service_id: None, sale_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 company_id pub fn company_id(mut self, company_id: u32) -> Self { self.company_id = Some(company_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 buyer_name pub fn buyer_name>(mut self, buyer_name: S) -> Self { self.buyer_name = Some(buyer_name.into()); self } /// Set the buyer_email pub fn buyer_email>(mut self, buyer_email: S) -> Self { self.buyer_email = Some(buyer_email.into()); 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: SaleStatus) -> Self { self.status = Some(status); self } /// Set the service_id pub fn service_id(mut self, service_id: u32) -> Self { self.service_id = Some(service_id); self } /// Set the sale_date pub fn sale_date(mut self, sale_date: DateTime) -> Self { self.sale_date = Some(sale_date); self } /// Add an item to the sale pub fn add_item(mut self, item: SaleItem) -> Self { self.items.push(item); self } /// Build the Sale 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 amounts let mut subtotal_amount = Currency { amount: 0.0, currency_code: currency_code.clone(), }; let mut tax_amount = Currency { amount: 0.0, currency_code: currency_code.clone(), }; let mut total_amount = Currency { amount: 0.0, currency_code: currency_code.clone(), }; // Calculate amounts from items for item in &self.items { // Make sure the item's sale_id matches this sale if item.sale_id != id { return Err("Item sale_id must match sale id"); } subtotal_amount.amount += item.subtotal.amount; tax_amount.amount += item.tax_amount.amount; } // Calculate total amount total_amount.amount = subtotal_amount.amount + tax_amount.amount; Ok(Sale { id, company_id: self.company_id.ok_or("company_id is required")?, customer_id: self.customer_id.ok_or("customer_id is required")?, buyer_name: self.buyer_name.ok_or("buyer_name is required")?, buyer_email: self.buyer_email.ok_or("buyer_email is required")?, subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount), tax_amount: self.tax_amount.unwrap_or(tax_amount), total_amount: self.total_amount.unwrap_or(total_amount), status: self.status.ok_or("status is required")?, service_id: self.service_id, sale_date: self.sale_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 Sale {} // Implement SledModel trait impl SledModel for Sale { fn get_id(&self) -> String { self.id.to_string() } fn db_prefix() -> &'static str { "sale" } }