580 lines
17 KiB
Rust
580 lines
17 KiB
Rust
use crate::db::{Model, Storable, DbError, DbResult, IndexKey};
|
|
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: Option<u32>, // ID of the product sold (if this is a product sale)
|
|
pub service_id: Option<u32>, // ID of the service sold (if this is a service sale)
|
|
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: Option<u32>,
|
|
service_id: Option<u32>,
|
|
name: String,
|
|
description: String,
|
|
comments: String,
|
|
quantity: i32,
|
|
unit_price: Currency,
|
|
tax_rate: f64,
|
|
active_till: DateTime<Utc>,
|
|
) -> Self {
|
|
// Validate that either product_id or service_id is provided, but not both
|
|
assert!(
|
|
(product_id.is_some() && service_id.is_none()) ||
|
|
(product_id.is_none() && service_id.is_some()),
|
|
"Either product_id or service_id must be provided, but not both"
|
|
);
|
|
// 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,
|
|
service_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>,
|
|
service_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,
|
|
service_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: Option<u32>) -> Self {
|
|
// If setting product_id, ensure service_id is None
|
|
if product_id.is_some() {
|
|
self.service_id = None;
|
|
}
|
|
self.product_id = product_id;
|
|
self
|
|
}
|
|
|
|
/// Set the service_id
|
|
pub fn service_id(mut self, service_id: Option<u32>) -> Self {
|
|
// If setting service_id, ensure product_id is None
|
|
if service_id.is_some() {
|
|
self.product_id = None;
|
|
}
|
|
self.service_id = service_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
|
|
|
|
// Validate that either product_id or service_id is provided, but not both
|
|
if self.product_id.is_none() && self.service_id.is_none() {
|
|
return Err("Either product_id or service_id must be provided");
|
|
}
|
|
if self.product_id.is_some() && self.service_id.is_some() {
|
|
return Err("Only one of product_id or service_id can be provided");
|
|
}
|
|
|
|
// 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,
|
|
service_id: self.service_id,
|
|
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 customer_id: u32, // ID of the customer making the purchase
|
|
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,
|
|
customer_id: u32,
|
|
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,
|
|
customer_id,
|
|
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();
|
|
}
|
|
|
|
/// Link this sale to an existing service
|
|
pub fn link_to_service(&mut self, service_id: u32) {
|
|
self.service_id = Some(service_id);
|
|
self.updated_at = Utc::now();
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
customer_id: self.customer_id.ok_or("customer_id 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"
|
|
}
|
|
|
|
fn db_keys(&self) -> Vec<IndexKey> {
|
|
let mut keys = Vec::new();
|
|
|
|
// Add an index for customer_id
|
|
keys.push(IndexKey {
|
|
name: "customer_id",
|
|
value: self.customer_id.to_string(),
|
|
});
|
|
|
|
|
|
|
|
keys
|
|
}
|
|
}
|
|
|