This commit is contained in:
despiegk 2025-04-19 13:53:41 +02:00
parent 6e9305d4e6
commit 5b5b64658c
5 changed files with 520 additions and 418 deletions

View File

@ -7,7 +7,7 @@
pub mod db;
pub mod error;
pub mod models;
// pub mod rhaiengine;
pub mod rhaiengine;
// Re-exports
pub use error::Error;

View File

@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization
## Model Relationships
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Currency │◄────┤ Product │◄────┤ SaleItem │
└─────────────┘ └─────────────┘ └──────┬──────┘
▲ │
│ │
┌─────┴──────────┐ │
│ProductComponent│ │
└────────────────┘ │
┌─────────────┐
│ Sale │
└─────────────┘
│ Customer │
└──────┬──────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
▲ │
│ │
┌─────┴──────────┐ │
│ProductComponent│ │
└────────────────┘ │
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐
│ InvoiceItem │◄────┤ Invoice │
└─────────────┘ └─────────────┘
```
## Business Logic Relationships
- **Customer**: The entity purchasing products or services
- **Product/Service**: Defines what is being sold, including its base price
- Can be marked as a template (`is_template=true`) to create copies for actual sales
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
- Can be linked to a Service when the sale creates an ongoing service
- **Service**: Represents an ongoing service provided to a customer
- Created from a Product template when the product type is Service
- **Invoice**: Represents the billing document for a sale, with payment tracking
- Created from a Sale object to handle billing and payment tracking
## Root Objects
- root objects are the one who are stored in the DB
- Root Objects are
- currency
- product
- Root objects are the ones stored directly in the DB
- Root Objects are:
- Customer
- Currency
- Product
- Sale
- Service
- Invoice
## Models
@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code.
**Builder:**
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
### Customer (Root Object)
Represents a customer who can purchase products or services.
**Properties:**
- `id`: u32 - Unique identifier
- `name`: String - Customer name
- `description`: String - Customer description
- `pubkey`: String - Customer's public key
- `contact_ids`: Vec<u32> - List of contact IDs
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Methods:**
- `add_contact()` - Adds a contact ID to the customer
- `remove_contact()` - Removes a contact ID from the customer
### Product
#### ProductType Enum
@ -70,11 +113,11 @@ Represents a component part of a product.
**Builder:**
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
#### Product (Root Object)
#### Product (Root Object)
Represents a product or service offered.
**Properties:**
- `id`: u32 - Unique identifier
- `id`: i64 - Unique identifier
- `name`: String - Product name
- `description`: String - Product description
- `price`: Currency - Product price
@ -83,10 +126,11 @@ Represents a product or service offered.
- `status`: ProductStatus - Available or Unavailable
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `max_amount`: u16 - Maximum quantity available
- `max_amount`: i64 - Maximum quantity available
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
- `active_till`: DateTime<Utc> - When product/service expires
- `components`: Vec<ProductComponent> - List of product components
- `is_template`: bool - Whether this is a template product (to be added)
**Methods:**
- `add_component()` - Adds a component to this product
@ -104,7 +148,62 @@ Represents a product or service offered.
- `get_id()` - Returns the ID as a string
- `db_prefix()` - Returns "product" as the database prefix
### Sale
### Service (Root Object)
#### BillingFrequency Enum
Defines how often a service is billed:
- `Hourly` - Billed by the hour
- `Daily` - Billed daily
- `Weekly` - Billed weekly
- `Monthly` - Billed monthly
- `Yearly` - Billed yearly
#### ServiceStatus Enum
Tracks the status of a service:
- `Active` - Service is currently active
- `Paused` - Service is temporarily paused
- `Cancelled` - Service has been cancelled
- `Completed` - Service has been completed
#### ServiceItem
Represents an item within a service.
**Properties:**
- `id`: u32 - Unique identifier
- `service_id`: u32 - Parent service ID
- `product_id`: u32 - ID of the product this service is based on
- `name`: String - Service name
- `description`: String - Detailed description of the service item
- `comments`: String - Additional notes or comments about the service item
- `quantity`: i32 - Number of units
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price before tax
- `tax_rate`: f64 - Tax rate as a percentage
- `tax_amount`: Currency - Calculated tax amount
- `is_taxable`: bool - Whether this item is taxable
- `active_till`: DateTime<Utc> - When service expires
#### Service
Represents an ongoing service provided to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer receiving the service
- `total_amount`: Currency - Total service amount including tax
- `status`: ServiceStatus - Current service status
- `billing_frequency`: BillingFrequency - How often the service is billed
- `service_date`: DateTime<Utc> - When service started
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<ServiceItem> - List of items in the service
- `is_template`: bool - Whether this is a template service (to be added)
**Methods:**
- `add_item()` - Adds an item to the service and updates total
- `calculate_total()` - Recalculates the total amount
- `update_status()` - Updates the status of the service
### Sale
#### SaleStatus Enum
Tracks the status of a sale:
@ -120,11 +219,18 @@ Represents an item within a sale.
- `sale_id`: u32 - Parent sale ID
- `product_id`: u32 - ID of the product sold
- `name`: String - Product name at time of sale
- `description`: String - Detailed description of the item
- `comments`: String - Additional notes or comments about the item
- `quantity`: i32 - Number of items purchased
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price for this item (calculated)
- `subtotal`: Currency - Total price for this item before tax (calculated)
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
- `tax_amount`: Currency - Calculated tax amount for this item
- `active_till`: DateTime<Utc> - When item/service expires
**Methods:**
- `total_with_tax()` - Returns the total amount including tax
**Builder:**
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
@ -134,18 +240,24 @@ Represents a complete sale transaction.
**Properties:**
- `id`: u32 - Unique identifier
- `company_id`: u32 - ID of the company making the sale
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
- `buyer_name`: String - Name of the buyer
- `buyer_email`: String - Email of the buyer
- `total_amount`: Currency - Total sale amount
- `subtotal_amount`: Currency - Total sale amount before tax
- `tax_amount`: Currency - Total tax amount for the sale
- `total_amount`: Currency - Total sale amount including tax
- `status`: SaleStatus - Current sale status
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
- `sale_date`: DateTime<Utc> - When sale occurred
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<SaleItem> - List of items in the sale
**Methods:**
- `add_item()` - Adds an item to the sale and updates total
- `add_item()` - Adds an item to the sale and updates totals
- `update_status()` - Updates the status of the sale
- `recalculate_totals()` - Recalculates all totals based on items
- `create_service()` - Creates a service from this sale (to be added)
**Builder:**
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
@ -223,6 +335,7 @@ let item = SaleItemBuilder::new()
.name("Premium Service")
.quantity(1)
.unit_price(unit_price)
.tax_rate(20.0) // 20% tax rate
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new()
// Update the sale status
sale.update_status(SaleStatus::Completed);
// The sale now contains:
// - subtotal_amount: The sum of all items before tax
// - tax_amount: The sum of all tax amounts
// - total_amount: The total including tax
```
### Relationship Between Sale and Invoice
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
```rust
// Create an invoice item linked to a sale
let invoice_item = InvoiceItemBuilder::new()
.id(1)
.invoice_id(1)
.description("Premium Service")
.amount(sale.total_amount.clone()) // Use the total amount from the sale
.sale_id(sale.id) // Link to the sale
.build()
.expect("Failed to build invoice item");
```
## Database Operations
@ -266,4 +402,125 @@ These methods are available for all root objects:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
### Invoice (Root Object)
#### InvoiceStatus Enum
Tracks the status of an invoice:
- `Draft` - Invoice is in draft state
- `Sent` - Invoice has been sent to the customer
- `Paid` - Invoice has been paid
- `Overdue` - Invoice is past due date
- `Cancelled` - Invoice has been cancelled
#### PaymentStatus Enum
Tracks the payment status of an invoice:
- `Unpaid` - Invoice has not been paid
- `PartiallyPaid` - Invoice has been partially paid
- `Paid` - Invoice has been fully paid
#### Payment
Represents a payment made against an invoice.
**Properties:**
- `amount`: Currency - Payment amount
- `date`: DateTime<Utc> - Payment date
- `method`: String - Payment method
- `comment`: String - Payment comment
#### InvoiceItem
Represents an item in an invoice.
**Properties:**
- `id`: u32 - Unique identifier
- `invoice_id`: u32 - Parent invoice ID
- `description`: String - Item description
- `amount`: Currency - Item amount
- `service_id`: Option<u32> - ID of the service this item is for
- `sale_id`: Option<u32> - ID of the sale this item is for
**Methods:**
- `link_to_service()` - Links the invoice item to a service
- `link_to_sale()` - Links the invoice item to a sale
#### Invoice
Represents an invoice sent to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer being invoiced
- `total_amount`: Currency - Total invoice amount
- `balance_due`: Currency - Amount still due
- `status`: InvoiceStatus - Current invoice status
- `payment_status`: PaymentStatus - Current payment status
- `issue_date`: DateTime<Utc> - When invoice was issued
- `due_date`: DateTime<Utc> - When payment is due
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<InvoiceItem> - List of items in the invoice
- `payments`: Vec<Payment> - List of payments made
**Methods:**
- `add_item()` - Adds an item to the invoice
- `calculate_total()` - Calculates the total amount
- `add_payment()` - Adds a payment to the invoice
- `calculate_balance()` - Calculates the balance due
- `update_payment_status()` - Updates the payment status
- `update_status()` - Updates the status of the invoice
- `is_overdue()` - Checks if the invoice is overdue
- `check_if_overdue()` - Marks the invoice as overdue if past due date
### Relationships Between Models
#### Product/Service Templates and Instances
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
#### Sale to Service Relationship
When a product of type `Service` is sold, a Service instance can be created from the Sale:
```rust
// Create a service from a sale
let service = sale.create_service(
service_id,
ServiceStatus::Active,
BillingFrequency::Monthly
);
```
#### Sale to Invoice Relationship
An Invoice is created from a Sale to handle billing and payment tracking:
```rust
// Create an invoice from a sale
let invoice = Invoice::from_sale(
invoice_id,
sale,
due_date
);
```
#### Customer-Centric View
The models allow tracking all customer interactions:
- What products/services they've purchased (via Sale records)
- What ongoing services they have (via Service records)
- What they've been invoiced for (via Invoice records)
- What they've paid (via Payment records in Invoices)
```rust
// Get all sales for a customer
let customer_sales = db.list_sales_by_customer(customer_id);
// Get all services for a customer
let customer_services = db.list_services_by_customer(customer_id);
// Get all invoices for a customer
let customer_invoices = db.list_invoices_by_customer(customer_id);
```

View File

@ -1,371 +0,0 @@
# Business Models Implementation Plan
## Overview
This document outlines the plan for implementing new business models in the codebase:
1. **Service**: For tracking recurring payments (similar to Sale)
2. **Customer**: For storing customer information
3. **Contract**: For linking services or sales to customers
4. **Invoice**: For invoicing customers
## Model Diagrams
### Core Models and Relationships
```mermaid
classDiagram
class Service {
+id: u32
+customer_id: u32
+total_amount: Currency
+status: ServiceStatus
+billing_frequency: BillingFrequency
+service_date: DateTime~Utc~
+created_at: DateTime~Utc~
+updated_at: DateTime~Utc~
+items: Vec~ServiceItem~
+calculate_total()
}
class ServiceItem {
+id: u32
+service_id: u32
+name: String
+quantity: i32
+unit_price: Currency
+subtotal: Currency
+tax_rate: f64
+tax_amount: Currency
+is_taxable: bool
+active_till: DateTime~Utc~
}
class Customer {
+id: u32
+name: String
+description: String
+pubkey: String
+contact_ids: Vec~u32~
+created_at: DateTime~Utc~
+updated_at: DateTime~Utc~
}
class Contract {
+id: u32
+customer_id: u32
+service_id: Option~u32~
+sale_id: Option~u32~
+terms: String
+start_date: DateTime~Utc~
+end_date: DateTime~Utc~
+auto_renewal: bool
+renewal_terms: String
+status: ContractStatus
+created_at: DateTime~Utc~
+updated_at: DateTime~Utc~
}
class Invoice {
+id: u32
+customer_id: u32
+total_amount: Currency
+balance_due: Currency
+status: InvoiceStatus
+payment_status: PaymentStatus
+issue_date: DateTime~Utc~
+due_date: DateTime~Utc~
+created_at: DateTime~Utc~
+updated_at: DateTime~Utc~
+items: Vec~InvoiceItem~
+payments: Vec~Payment~
}
class InvoiceItem {
+id: u32
+invoice_id: u32
+description: String
+amount: Currency
+service_id: Option~u32~
+sale_id: Option~u32~
}
class Payment {
+amount: Currency
+date: DateTime~Utc~
+method: String
}
Service "1" -- "many" ServiceItem : contains
Customer "1" -- "many" Service : has
Customer "1" -- "many" Contract : has
Contract "1" -- "0..1" Service : references
Contract "1" -- "0..1" Sale : references
Invoice "1" -- "many" InvoiceItem : contains
Invoice "1" -- "many" Payment : contains
Customer "1" -- "many" Invoice : has
InvoiceItem "1" -- "0..1" Service : references
InvoiceItem "1" -- "0..1" Sale : references
```
### Enums and Supporting Types
```mermaid
classDiagram
class BillingFrequency {
<<enumeration>>
Hourly
Daily
Weekly
Monthly
Yearly
}
class ServiceStatus {
<<enumeration>>
Active
Paused
Cancelled
Completed
}
class ContractStatus {
<<enumeration>>
Active
Expired
Terminated
}
class InvoiceStatus {
<<enumeration>>
Draft
Sent
Paid
Overdue
Cancelled
}
class PaymentStatus {
<<enumeration>>
Unpaid
PartiallyPaid
Paid
}
Service -- ServiceStatus : has
Service -- BillingFrequency : has
Contract -- ContractStatus : has
Invoice -- InvoiceStatus : has
Invoice -- PaymentStatus : has
```
## Detailed Implementation Plan
### 1. Service and ServiceItem (service.rs)
The Service model will be similar to Sale but designed for recurring payments:
- **Service**: Main struct for tracking recurring services
- Fields:
- id: u32
- customer_id: u32
- total_amount: Currency
- status: ServiceStatus
- billing_frequency: BillingFrequency
- service_date: DateTime<Utc>
- created_at: DateTime<Utc>
- updated_at: DateTime<Utc>
- items: Vec<ServiceItem>
- Methods:
- calculate_total(): Updates the total_amount based on all items
- add_item(item: ServiceItem): Adds an item and updates the total
- update_status(status: ServiceStatus): Updates the status and timestamp
- **ServiceItem**: Items within a service (similar to SaleItem)
- Fields:
- id: u32
- service_id: u32
- name: String
- quantity: i32
- unit_price: Currency
- subtotal: Currency
- tax_rate: f64
- tax_amount: Currency
- is_taxable: bool
- active_till: DateTime<Utc>
- Methods:
- calculate_subtotal(): Calculates subtotal based on quantity and unit_price
- calculate_tax(): Calculates tax amount based on subtotal and tax_rate
- **BillingFrequency**: Enum for different billing periods
- Variants: Hourly, Daily, Weekly, Monthly, Yearly
- **ServiceStatus**: Enum for service status
- Variants: Active, Paused, Cancelled, Completed
### 2. Customer (customer.rs)
The Customer model will store customer information:
- **Customer**: Main struct for customer data
- Fields:
- id: u32
- name: String
- description: String
- pubkey: String
- contact_ids: Vec<u32>
- created_at: DateTime<Utc>
- updated_at: DateTime<Utc>
- Methods:
- add_contact(contact_id: u32): Adds a contact ID to the list
- remove_contact(contact_id: u32): Removes a contact ID from the list
### 3. Contract (contract.rs)
The Contract model will link services or sales to customers:
- **Contract**: Main struct for contract data
- Fields:
- id: u32
- customer_id: u32
- service_id: Option<u32>
- sale_id: Option<u32>
- terms: String
- start_date: DateTime<Utc>
- end_date: DateTime<Utc>
- auto_renewal: bool
- renewal_terms: String
- status: ContractStatus
- created_at: DateTime<Utc>
- updated_at: DateTime<Utc>
- Methods:
- is_active(): bool - Checks if the contract is currently active
- is_expired(): bool - Checks if the contract has expired
- renew(): Updates the contract dates based on renewal terms
- **ContractStatus**: Enum for contract status
- Variants: Active, Expired, Terminated
### 4. Invoice (invoice.rs)
The Invoice model will handle billing:
- **Invoice**: Main struct for invoice data
- Fields:
- id: u32
- customer_id: u32
- total_amount: Currency
- balance_due: Currency
- status: InvoiceStatus
- payment_status: PaymentStatus
- issue_date: DateTime<Utc>
- due_date: DateTime<Utc>
- created_at: DateTime<Utc>
- updated_at: DateTime<Utc>
- items: Vec<InvoiceItem>
- payments: Vec<Payment>
- Methods:
- calculate_total(): Updates the total_amount based on all items
- add_item(item: InvoiceItem): Adds an item and updates the total
- add_payment(payment: Payment): Adds a payment and updates balance_due and payment_status
- update_status(status: InvoiceStatus): Updates the status and timestamp
- calculate_balance(): Updates the balance_due based on total_amount and payments
- **InvoiceItem**: Items within an invoice
- Fields:
- id: u32
- invoice_id: u32
- description: String
- amount: Currency
- service_id: Option<u32>
- sale_id: Option<u32>
- **Payment**: Struct for tracking payments
- Fields:
- amount: Currency
- date: DateTime<Utc>
- method: String
- **InvoiceStatus**: Enum for invoice status
- Variants: Draft, Sent, Paid, Overdue, Cancelled
- **PaymentStatus**: Enum for payment status
- Variants: Unpaid, PartiallyPaid, Paid
### 5. Updates to mod.rs
We'll need to update the mod.rs file to include the new modules and re-export the types:
```rust
pub mod currency;
pub mod product;
pub mod sale;
pub mod exchange_rate;
pub mod service;
pub mod customer;
pub mod contract;
pub mod invoice;
// Re-export all model types for convenience
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::{Sale, SaleItem, SaleStatus};
pub use currency::Currency;
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency};
pub use customer::Customer;
pub use contract::{Contract, ContractStatus};
pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment};
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
pub use currency::CurrencyBuilder;
pub use exchange_rate::ExchangeRateBuilder;
pub use service::{ServiceBuilder, ServiceItemBuilder};
pub use customer::CustomerBuilder;
pub use contract::ContractBuilder;
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};
```
### 6. Updates to model_methods.rs
We'll need to update the model_methods.rs file to implement the model methods for the new models:
```rust
use crate::db::db::DB;
use crate::db::base::{SledDBResult, SledModel};
use crate::impl_model_methods;
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
// Implement model-specific methods for Product
impl_model_methods!(Product, product, products);
// Implement model-specific methods for Sale
impl_model_methods!(Sale, sale, sales);
// Implement model-specific methods for Currency
impl_model_methods!(Currency, currency, currencies);
// Implement model-specific methods for ExchangeRate
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
// Implement model-specific methods for Service
impl_model_methods!(Service, service, services);
// Implement model-specific methods for Customer
impl_model_methods!(Customer, customer, customers);
// Implement model-specific methods for Contract
impl_model_methods!(Contract, contract, contracts);
// Implement model-specific methods for Invoice
impl_model_methods!(Invoice, invoice, invoices);
```
## Implementation Approach
1. Create the new model files (service.rs, customer.rs, contract.rs, invoice.rs)
2. Implement the structs, enums, and methods for each model
3. Update mod.rs to include the new modules and re-export the types
4. Update model_methods.rs to implement the model methods for the new models
5. Test the new models with example code

View File

@ -21,9 +21,13 @@ pub struct SaleItem {
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
}
@ -34,28 +38,50 @@ impl SaleItem {
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
// 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
@ -65,9 +91,13 @@ pub struct SaleItemBuilder {
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>>,
}
@ -79,9 +109,13 @@ impl SaleItemBuilder {
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,
}
}
@ -109,6 +143,18 @@ impl SaleItemBuilder {
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 {
@ -122,6 +168,12 @@ impl SaleItemBuilder {
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);
@ -132,6 +184,7 @@ impl SaleItemBuilder {
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;
@ -139,15 +192,26 @@ impl SaleItemBuilder {
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")?,
})
}
@ -158,10 +222,14 @@ impl SaleItemBuilder {
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 total_amount: Currency,
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>,
@ -175,22 +243,29 @@ impl Sale {
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,
total_amount: Currency {
amount: 0.0,
currency_code,
},
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,
@ -203,17 +278,27 @@ impl Sale {
// 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 total amount
// Update the amounts
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency {
// 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 total
// Add to the existing totals
// (Assumes all items have the same currency)
self.total_amount.amount += item.subtotal.amount;
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
@ -222,12 +307,93 @@ impl Sale {
// 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
@ -235,10 +401,14 @@ impl Sale {
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>>,
@ -252,10 +422,14 @@ impl SaleBuilder {
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,
@ -275,6 +449,12 @@ impl SaleBuilder {
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 {
@ -299,6 +479,12 @@ impl SaleBuilder {
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 {
@ -318,39 +504,45 @@ impl SaleBuilder {
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
// 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 total amount from items
// 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");
}
if total_amount.amount == 0.0 {
// First item, initialize the total amount with the same currency
total_amount = Currency {
amount: item.subtotal.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;
}
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),

View File

@ -29,6 +29,8 @@ pub struct ServiceItem {
pub service_id: u32,
pub product_id: u32,
pub name: String,
pub description: String, // Description of the service item
pub comments: String, // Additional comments about the service item
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
@ -45,6 +47,8 @@ impl ServiceItem {
service_id: u32,
product_id: u32,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
tax_rate: f64,
@ -76,6 +80,8 @@ impl ServiceItem {
service_id,
product_id,
name,
description,
comments,
quantity,
unit_price,
subtotal,
@ -117,6 +123,8 @@ pub struct ServiceItemBuilder {
service_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>,
@ -134,6 +142,8 @@ impl ServiceItemBuilder {
service_id: None,
product_id: None,
name: None,
description: None,
comments: None,
quantity: None,
unit_price: None,
subtotal: None,
@ -167,6 +177,18 @@ impl ServiceItemBuilder {
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 {
@ -230,6 +252,8 @@ impl ServiceItemBuilder {
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")?,
description: self.description.unwrap_or_default(),
comments: self.comments.unwrap_or_default(),
quantity,
unit_price,
subtotal,