db/herodb/src/models/biz/sale.rs
2025-04-20 09:21:32 +02:00

583 lines
17 KiB
Rust

use crate::db::{Model, Storable, DbError, DbResult};
use crate::models::biz::Currency; // Use crate:: for importing from the 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::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
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::new(
0, // Use 0 as a temporary ID
self.subtotal.amount + self.tax_amount.amount,
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::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
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::new(
0, // Use 0 as a temporary ID
0.0,
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::new(
0, // Use 0 as a temporary ID
item.subtotal.amount,
item.subtotal.currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
item.tax_amount.amount,
item.tax_amount.currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount + item.tax_amount.amount,
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::new(
0, // Use 0 as a temporary ID
subtotal,
currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_total,
currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
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::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut tax_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
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
impl Storable for Sale {}
// Implement Model trait
impl Model for Sale {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"sale"
}
}