567 lines
17 KiB
Rust
567 lines
17 KiB
Rust
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<Utc>, // 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<Utc>,
|
|
) -> 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<u32>,
|
|
sale_id: Option<u32>,
|
|
product_id: Option<u32>,
|
|
name: Option<String>,
|
|
description: Option<String>,
|
|
comments: Option<String>,
|
|
quantity: Option<i32>,
|
|
unit_price: Option<Currency>,
|
|
subtotal: Option<Currency>,
|
|
tax_rate: Option<f64>,
|
|
tax_amount: Option<Currency>,
|
|
active_till: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
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<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 comments
|
|
pub fn comments<S: Into<String>>(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<Utc>) -> Self {
|
|
self.active_till = Some(active_till);
|
|
self
|
|
}
|
|
|
|
/// Build the SaleItem object
|
|
pub fn build(self) -> Result<SaleItem, &'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); // 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<u32>, // ID of the service created from this sale (if applicable)
|
|
pub sale_date: DateTime<Utc>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
pub items: Vec<SaleItem>,
|
|
}
|
|
|
|
// 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<crate::models::biz::Service, &'static str> {
|
|
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<u32>,
|
|
company_id: Option<u32>,
|
|
customer_id: Option<u32>,
|
|
buyer_name: Option<String>,
|
|
buyer_email: Option<String>,
|
|
subtotal_amount: Option<Currency>,
|
|
tax_amount: Option<Currency>,
|
|
total_amount: Option<Currency>,
|
|
status: Option<SaleStatus>,
|
|
service_id: Option<u32>,
|
|
sale_date: Option<DateTime<Utc>>,
|
|
created_at: Option<DateTime<Utc>>,
|
|
updated_at: Option<DateTime<Utc>>,
|
|
items: Vec<SaleItem>,
|
|
currency_code: Option<String>,
|
|
}
|
|
|
|
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<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
|
self.buyer_name = Some(buyer_name.into());
|
|
self
|
|
}
|
|
|
|
/// Set the buyer_email
|
|
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
|
|
self.buyer_email = Some(buyer_email.into());
|
|
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: 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<Utc>) -> 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<Sale, &'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 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"
|
|
}
|
|
}
|