From 5b5b64658c73a617dba0f32d98f8ef82bb0a9775 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 19 Apr 2025 13:53:41 +0200 Subject: [PATCH] ... --- herodb/src/lib.rs | 2 +- herodb/src/models/biz/README.md | 301 ++++++++++++-- herodb/src/models/biz/business_models_plan.md | 371 ------------------ herodb/src/models/biz/sale.rs | 240 +++++++++-- herodb/src/models/biz/service.rs | 24 ++ 5 files changed, 520 insertions(+), 418 deletions(-) delete mode 100644 herodb/src/models/biz/business_models_plan.md diff --git a/herodb/src/lib.rs b/herodb/src/lib.rs index f5b6a46..9e1771a 100644 --- a/herodb/src/lib.rs +++ b/herodb/src/lib.rs @@ -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; diff --git a/herodb/src/models/biz/README.md b/herodb/src/models/biz/README.md index 4aa684c..b6a9859 100644 --- a/herodb/src/models/biz/README.md +++ b/herodb/src/models/biz/README.md @@ -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 - List of contact IDs +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - 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 - Creation timestamp - `updated_at`: DateTime - Last update timestamp -- `max_amount`: u16 - Maximum quantity available +- `max_amount`: i64 - Maximum quantity available - `purchase_till`: DateTime - Deadline for purchasing - `active_till`: DateTime - When product/service expires - `components`: Vec - 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 - 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 - When service started +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - Last update timestamp +- `items`: Vec - 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 - 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 - ID of the service created from this sale (to be added) - `sale_date`: DateTime - When sale occurred - `created_at`: DateTime - Creation timestamp - `updated_at`: DateTime - Last update timestamp - `items`: Vec - 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 - 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 - ID of the service this item is for +- `sale_id`: Option - 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 - When invoice was issued +- `due_date`: DateTime - When payment is due +- `created_at`: DateTime - Creation timestamp +- `updated_at`: DateTime - Last update timestamp +- `items`: Vec - List of items in the invoice +- `payments`: Vec - 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); +``` diff --git a/herodb/src/models/biz/business_models_plan.md b/herodb/src/models/biz/business_models_plan.md deleted file mode 100644 index d68ec76..0000000 --- a/herodb/src/models/biz/business_models_plan.md +++ /dev/null @@ -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 { - <> - Hourly - Daily - Weekly - Monthly - Yearly - } - - class ServiceStatus { - <> - Active - Paused - Cancelled - Completed - } - - class ContractStatus { - <> - Active - Expired - Terminated - } - - class InvoiceStatus { - <> - Draft - Sent - Paid - Overdue - Cancelled - } - - class PaymentStatus { - <> - 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 - - created_at: DateTime - - updated_at: DateTime - - items: Vec - - 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 - - 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 - - created_at: DateTime - - updated_at: DateTime - - 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 - - sale_id: Option - - terms: String - - start_date: DateTime - - end_date: DateTime - - auto_renewal: bool - - renewal_terms: String - - status: ContractStatus - - created_at: DateTime - - updated_at: DateTime - - 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 - - due_date: DateTime - - created_at: DateTime - - updated_at: DateTime - - items: Vec - - payments: Vec - - 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 - - sale_id: Option - -- **Payment**: Struct for tracking payments - - Fields: - - amount: Currency - - date: DateTime - - 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 \ No newline at end of file diff --git a/herodb/src/models/biz/sale.rs b/herodb/src/models/biz/sale.rs index dda10f7..00f7c43 100644 --- a/herodb/src/models/biz/sale.rs +++ b/herodb/src/models/biz/sale.rs @@ -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, // 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, ) -> 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, 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>, } @@ -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>(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 { @@ -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) -> Self { self.active_till = Some(active_till); @@ -132,6 +184,7 @@ impl SaleItemBuilder { 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; @@ -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, // ID of the service created from this sale (if applicable) pub sale_date: DateTime, pub created_at: DateTime, pub updated_at: DateTime, @@ -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 { + 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, 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>, @@ -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>(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) -> 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), diff --git a/herodb/src/models/biz/service.rs b/herodb/src/models/biz/service.rs index 2c1eb05..c22b57f 100644 --- a/herodb/src/models/biz/service.rs +++ b/herodb/src/models/biz/service.rs @@ -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, product_id: Option, name: Option, + description: Option, + comments: Option, quantity: Option, unit_price: Option, subtotal: Option, @@ -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>(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 { @@ -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,