...
This commit is contained in:
parent
6e9305d4e6
commit
5b5b64658c
@ -7,7 +7,7 @@
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
// pub mod rhaiengine;
|
pub mod rhaiengine;
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization
|
|||||||
## Model Relationships
|
## Model Relationships
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐
|
||||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │
|
│ Customer │
|
||||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
||||||
▲ │
|
▲ │
|
||||||
│ │
|
│ │
|
||||||
┌─────┴──────────┐ │
|
┌─────┴──────────┐ │
|
||||||
│ProductComponent│ │
|
│ProductComponent│ │
|
||||||
└────────────────┘ │
|
└────────────────┘ │
|
||||||
|
│
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────┐
|
┌─────────────┐ ┌─────────────┐
|
||||||
│ Sale │
|
│ 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
|
||||||
|
|
||||||
- root objects are the one who are stored in the DB
|
- Root objects are the ones stored directly in the DB
|
||||||
- Root Objects are
|
- Root Objects are:
|
||||||
- currency
|
- Customer
|
||||||
- product
|
- Currency
|
||||||
|
- Product
|
||||||
- Sale
|
- Sale
|
||||||
|
- Service
|
||||||
|
- Invoice
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code.
|
|||||||
**Builder:**
|
**Builder:**
|
||||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
- `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
|
### Product
|
||||||
|
|
||||||
#### ProductType Enum
|
#### ProductType Enum
|
||||||
@ -74,7 +117,7 @@ Represents a component part of a product.
|
|||||||
Represents a product or service offered.
|
Represents a product or service offered.
|
||||||
|
|
||||||
**Properties:**
|
**Properties:**
|
||||||
- `id`: u32 - Unique identifier
|
- `id`: i64 - Unique identifier
|
||||||
- `name`: String - Product name
|
- `name`: String - Product name
|
||||||
- `description`: String - Product description
|
- `description`: String - Product description
|
||||||
- `price`: Currency - Product price
|
- `price`: Currency - Product price
|
||||||
@ -83,10 +126,11 @@ Represents a product or service offered.
|
|||||||
- `status`: ProductStatus - Available or Unavailable
|
- `status`: ProductStatus - Available or Unavailable
|
||||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||||
- `updated_at`: DateTime<Utc> - Last update 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
|
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||||
- `active_till`: DateTime<Utc> - When product/service expires
|
- `active_till`: DateTime<Utc> - When product/service expires
|
||||||
- `components`: Vec<ProductComponent> - List of product components
|
- `components`: Vec<ProductComponent> - List of product components
|
||||||
|
- `is_template`: bool - Whether this is a template product (to be added)
|
||||||
|
|
||||||
**Methods:**
|
**Methods:**
|
||||||
- `add_component()` - Adds a component to this product
|
- `add_component()` - Adds a component to this product
|
||||||
@ -104,6 +148,61 @@ Represents a product or service offered.
|
|||||||
- `get_id()` - Returns the ID as a string
|
- `get_id()` - Returns the ID as a string
|
||||||
- `db_prefix()` - Returns "product" as the database prefix
|
- `db_prefix()` - Returns "product" as the database prefix
|
||||||
|
|
||||||
|
### 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
|
### Sale
|
||||||
|
|
||||||
#### SaleStatus Enum
|
#### SaleStatus Enum
|
||||||
@ -120,11 +219,18 @@ Represents an item within a sale.
|
|||||||
- `sale_id`: u32 - Parent sale ID
|
- `sale_id`: u32 - Parent sale ID
|
||||||
- `product_id`: u32 - ID of the product sold
|
- `product_id`: u32 - ID of the product sold
|
||||||
- `name`: String - Product name at time of sale
|
- `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
|
- `quantity`: i32 - Number of items purchased
|
||||||
- `unit_price`: Currency - Price per unit
|
- `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
|
- `active_till`: DateTime<Utc> - When item/service expires
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- `total_with_tax()` - Returns the total amount including tax
|
||||||
|
|
||||||
**Builder:**
|
**Builder:**
|
||||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||||
|
|
||||||
@ -134,18 +240,24 @@ Represents a complete sale transaction.
|
|||||||
**Properties:**
|
**Properties:**
|
||||||
- `id`: u32 - Unique identifier
|
- `id`: u32 - Unique identifier
|
||||||
- `company_id`: u32 - ID of the company making the sale
|
- `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_name`: String - Name of the buyer
|
||||||
- `buyer_email`: String - Email 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
|
- `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
|
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||||
- `items`: Vec<SaleItem> - List of items in the sale
|
- `items`: Vec<SaleItem> - List of items in the sale
|
||||||
|
|
||||||
**Methods:**
|
**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
|
- `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:**
|
**Builder:**
|
||||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||||
@ -223,6 +335,7 @@ let item = SaleItemBuilder::new()
|
|||||||
.name("Premium Service")
|
.name("Premium Service")
|
||||||
.quantity(1)
|
.quantity(1)
|
||||||
.unit_price(unit_price)
|
.unit_price(unit_price)
|
||||||
|
.tax_rate(20.0) // 20% tax rate
|
||||||
.active_till(now + Duration::days(30))
|
.active_till(now + Duration::days(30))
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to build sale item");
|
.expect("Failed to build sale item");
|
||||||
@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new()
|
|||||||
|
|
||||||
// Update the sale status
|
// Update the sale status
|
||||||
sale.update_status(SaleStatus::Completed);
|
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
|
## 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_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
- `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);
|
||||||
|
```
|
||||||
|
@ -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
|
|
@ -21,9 +21,13 @@ pub struct SaleItem {
|
|||||||
pub sale_id: u32,
|
pub sale_id: u32,
|
||||||
pub product_id: u32,
|
pub product_id: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String, // Description of the item
|
||||||
|
pub comments: String, // Additional comments about the item
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub unit_price: Currency,
|
pub unit_price: Currency,
|
||||||
pub subtotal: 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
|
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,
|
sale_id: u32,
|
||||||
product_id: u32,
|
product_id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
description: String,
|
||||||
|
comments: String,
|
||||||
quantity: i32,
|
quantity: i32,
|
||||||
unit_price: Currency,
|
unit_price: Currency,
|
||||||
|
tax_rate: f64,
|
||||||
active_till: DateTime<Utc>,
|
active_till: DateTime<Utc>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Calculate subtotal
|
// Calculate subtotal (before tax)
|
||||||
let amount = unit_price.amount * quantity as f64;
|
let amount = unit_price.amount * quantity as f64;
|
||||||
let subtotal = Currency {
|
let subtotal = Currency {
|
||||||
amount,
|
amount,
|
||||||
currency_code: unit_price.currency_code.clone(),
|
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 {
|
Self {
|
||||||
id,
|
id,
|
||||||
sale_id,
|
sale_id,
|
||||||
product_id,
|
product_id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
|
comments,
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
active_till,
|
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
|
/// Builder for SaleItem
|
||||||
@ -65,9 +91,13 @@ pub struct SaleItemBuilder {
|
|||||||
sale_id: Option<u32>,
|
sale_id: Option<u32>,
|
||||||
product_id: Option<u32>,
|
product_id: Option<u32>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
comments: Option<String>,
|
||||||
quantity: Option<i32>,
|
quantity: Option<i32>,
|
||||||
unit_price: Option<Currency>,
|
unit_price: Option<Currency>,
|
||||||
subtotal: Option<Currency>,
|
subtotal: Option<Currency>,
|
||||||
|
tax_rate: Option<f64>,
|
||||||
|
tax_amount: Option<Currency>,
|
||||||
active_till: Option<DateTime<Utc>>,
|
active_till: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,9 +109,13 @@ impl SaleItemBuilder {
|
|||||||
sale_id: None,
|
sale_id: None,
|
||||||
product_id: None,
|
product_id: None,
|
||||||
name: None,
|
name: None,
|
||||||
|
description: None,
|
||||||
|
comments: None,
|
||||||
quantity: None,
|
quantity: None,
|
||||||
unit_price: None,
|
unit_price: None,
|
||||||
subtotal: None,
|
subtotal: None,
|
||||||
|
tax_rate: None,
|
||||||
|
tax_amount: None,
|
||||||
active_till: None,
|
active_till: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +144,18 @@ impl SaleItemBuilder {
|
|||||||
self
|
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
|
/// Set the quantity
|
||||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||||
self.quantity = Some(quantity);
|
self.quantity = Some(quantity);
|
||||||
@ -122,6 +168,12 @@ impl SaleItemBuilder {
|
|||||||
self
|
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
|
/// Set the active_till
|
||||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||||
self.active_till = Some(active_till);
|
self.active_till = Some(active_till);
|
||||||
@ -132,6 +184,7 @@ impl SaleItemBuilder {
|
|||||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||||
let quantity = self.quantity.ok_or("quantity 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
|
// Calculate subtotal
|
||||||
let amount = unit_price.amount * quantity as f64;
|
let amount = unit_price.amount * quantity as f64;
|
||||||
@ -140,14 +193,25 @@ impl SaleItemBuilder {
|
|||||||
currency_code: unit_price.currency_code.clone(),
|
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 {
|
Ok(SaleItem {
|
||||||
id: self.id.ok_or("id is required")?,
|
id: self.id.ok_or("id is required")?,
|
||||||
sale_id: self.sale_id.ok_or("sale_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")?,
|
product_id: self.product_id.ok_or("product_id is required")?,
|
||||||
name: self.name.ok_or("name is required")?,
|
name: self.name.ok_or("name is required")?,
|
||||||
|
description: self.description.unwrap_or_default(),
|
||||||
|
comments: self.comments.unwrap_or_default(),
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
tax_rate,
|
||||||
|
tax_amount,
|
||||||
active_till: self.active_till.ok_or("active_till is required")?,
|
active_till: self.active_till.ok_or("active_till is required")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -158,10 +222,14 @@ impl SaleItemBuilder {
|
|||||||
pub struct Sale {
|
pub struct Sale {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub company_id: u32,
|
pub company_id: u32,
|
||||||
|
pub customer_id: u32, // ID of the customer making the purchase
|
||||||
pub buyer_name: String,
|
pub buyer_name: String,
|
||||||
pub buyer_email: 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 status: SaleStatus,
|
||||||
|
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||||
pub sale_date: DateTime<Utc>,
|
pub sale_date: DateTime<Utc>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
@ -175,22 +243,29 @@ impl Sale {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
id: u32,
|
id: u32,
|
||||||
company_id: u32,
|
company_id: u32,
|
||||||
|
customer_id: u32,
|
||||||
buyer_name: String,
|
buyer_name: String,
|
||||||
buyer_email: String,
|
buyer_email: String,
|
||||||
currency_code: String,
|
currency_code: String,
|
||||||
status: SaleStatus,
|
status: SaleStatus,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
let zero_currency = Currency {
|
||||||
|
amount: 0.0,
|
||||||
|
currency_code: currency_code.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
company_id,
|
company_id,
|
||||||
|
customer_id,
|
||||||
buyer_name,
|
buyer_name,
|
||||||
buyer_email,
|
buyer_email,
|
||||||
total_amount: Currency {
|
subtotal_amount: zero_currency.clone(),
|
||||||
amount: 0.0,
|
tax_amount: zero_currency.clone(),
|
||||||
currency_code,
|
total_amount: zero_currency,
|
||||||
},
|
|
||||||
status,
|
status,
|
||||||
|
service_id: None,
|
||||||
sale_date: now,
|
sale_date: now,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@ -203,17 +278,27 @@ impl Sale {
|
|||||||
// Make sure the item's sale_id matches this 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");
|
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() {
|
if self.items.is_empty() {
|
||||||
// First item, initialize the total amount with the same currency
|
// First item, initialize the amounts with the same currency
|
||||||
self.total_amount = Currency {
|
self.subtotal_amount = Currency {
|
||||||
amount: item.subtotal.amount,
|
amount: item.subtotal.amount,
|
||||||
currency_code: item.subtotal.currency_code.clone(),
|
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 {
|
} else {
|
||||||
// Add to the existing total
|
// Add to the existing totals
|
||||||
// (Assumes all items have the same currency)
|
// (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
|
// Add the item to the list
|
||||||
@ -223,11 +308,92 @@ impl Sale {
|
|||||||
self.updated_at = Utc::now();
|
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
|
/// Update the status of the sale
|
||||||
pub fn update_status(&mut self, status: SaleStatus) {
|
pub fn update_status(&mut self, status: SaleStatus) {
|
||||||
self.status = status;
|
self.status = status;
|
||||||
self.updated_at = Utc::now();
|
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
|
/// Builder for Sale
|
||||||
@ -235,10 +401,14 @@ impl Sale {
|
|||||||
pub struct SaleBuilder {
|
pub struct SaleBuilder {
|
||||||
id: Option<u32>,
|
id: Option<u32>,
|
||||||
company_id: Option<u32>,
|
company_id: Option<u32>,
|
||||||
|
customer_id: Option<u32>,
|
||||||
buyer_name: Option<String>,
|
buyer_name: Option<String>,
|
||||||
buyer_email: Option<String>,
|
buyer_email: Option<String>,
|
||||||
|
subtotal_amount: Option<Currency>,
|
||||||
|
tax_amount: Option<Currency>,
|
||||||
total_amount: Option<Currency>,
|
total_amount: Option<Currency>,
|
||||||
status: Option<SaleStatus>,
|
status: Option<SaleStatus>,
|
||||||
|
service_id: Option<u32>,
|
||||||
sale_date: Option<DateTime<Utc>>,
|
sale_date: Option<DateTime<Utc>>,
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
updated_at: Option<DateTime<Utc>>,
|
updated_at: Option<DateTime<Utc>>,
|
||||||
@ -252,10 +422,14 @@ impl SaleBuilder {
|
|||||||
Self {
|
Self {
|
||||||
id: None,
|
id: None,
|
||||||
company_id: None,
|
company_id: None,
|
||||||
|
customer_id: None,
|
||||||
buyer_name: None,
|
buyer_name: None,
|
||||||
buyer_email: None,
|
buyer_email: None,
|
||||||
|
subtotal_amount: None,
|
||||||
|
tax_amount: None,
|
||||||
total_amount: None,
|
total_amount: None,
|
||||||
status: None,
|
status: None,
|
||||||
|
service_id: None,
|
||||||
sale_date: None,
|
sale_date: None,
|
||||||
created_at: None,
|
created_at: None,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
@ -276,6 +450,12 @@ impl SaleBuilder {
|
|||||||
self
|
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
|
/// Set the buyer_name
|
||||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||||
self.buyer_name = Some(buyer_name.into());
|
self.buyer_name = Some(buyer_name.into());
|
||||||
@ -300,6 +480,12 @@ impl SaleBuilder {
|
|||||||
self
|
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
|
/// Set the sale_date
|
||||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||||
self.sale_date = Some(sale_date);
|
self.sale_date = Some(sale_date);
|
||||||
@ -318,39 +504,45 @@ impl SaleBuilder {
|
|||||||
let id = self.id.ok_or("id is required")?;
|
let id = self.id.ok_or("id is required")?;
|
||||||
let currency_code = self.currency_code.ok_or("currency_code 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 {
|
let mut total_amount = Currency {
|
||||||
amount: 0.0,
|
amount: 0.0,
|
||||||
currency_code: currency_code.clone(),
|
currency_code: currency_code.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total amount from items
|
// Calculate amounts from items
|
||||||
for item in &self.items {
|
for item in &self.items {
|
||||||
// Make sure the item's sale_id matches this sale
|
// Make sure the item's sale_id matches this sale
|
||||||
if item.sale_id != id {
|
if item.sale_id != id {
|
||||||
return Err("Item sale_id must match sale id");
|
return Err("Item sale_id must match sale id");
|
||||||
}
|
}
|
||||||
|
|
||||||
if total_amount.amount == 0.0 {
|
subtotal_amount.amount += item.subtotal.amount;
|
||||||
// First item, initialize the total amount with the same currency
|
tax_amount.amount += item.tax_amount.amount;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total amount
|
||||||
|
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||||
|
|
||||||
Ok(Sale {
|
Ok(Sale {
|
||||||
id,
|
id,
|
||||||
company_id: self.company_id.ok_or("company_id is required")?,
|
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_name: self.buyer_name.ok_or("buyer_name is required")?,
|
||||||
buyer_email: self.buyer_email.ok_or("buyer_email 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),
|
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||||
status: self.status.ok_or("status is required")?,
|
status: self.status.ok_or("status is required")?,
|
||||||
|
service_id: self.service_id,
|
||||||
sale_date: self.sale_date.unwrap_or(now),
|
sale_date: self.sale_date.unwrap_or(now),
|
||||||
created_at: self.created_at.unwrap_or(now),
|
created_at: self.created_at.unwrap_or(now),
|
||||||
updated_at: self.updated_at.unwrap_or(now),
|
updated_at: self.updated_at.unwrap_or(now),
|
||||||
|
@ -29,6 +29,8 @@ pub struct ServiceItem {
|
|||||||
pub service_id: u32,
|
pub service_id: u32,
|
||||||
pub product_id: u32,
|
pub product_id: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String, // Description of the service item
|
||||||
|
pub comments: String, // Additional comments about the service item
|
||||||
pub quantity: i32,
|
pub quantity: i32,
|
||||||
pub unit_price: Currency,
|
pub unit_price: Currency,
|
||||||
pub subtotal: Currency,
|
pub subtotal: Currency,
|
||||||
@ -45,6 +47,8 @@ impl ServiceItem {
|
|||||||
service_id: u32,
|
service_id: u32,
|
||||||
product_id: u32,
|
product_id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
description: String,
|
||||||
|
comments: String,
|
||||||
quantity: i32,
|
quantity: i32,
|
||||||
unit_price: Currency,
|
unit_price: Currency,
|
||||||
tax_rate: f64,
|
tax_rate: f64,
|
||||||
@ -76,6 +80,8 @@ impl ServiceItem {
|
|||||||
service_id,
|
service_id,
|
||||||
product_id,
|
product_id,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
|
comments,
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
@ -117,6 +123,8 @@ pub struct ServiceItemBuilder {
|
|||||||
service_id: Option<u32>,
|
service_id: Option<u32>,
|
||||||
product_id: Option<u32>,
|
product_id: Option<u32>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
comments: Option<String>,
|
||||||
quantity: Option<i32>,
|
quantity: Option<i32>,
|
||||||
unit_price: Option<Currency>,
|
unit_price: Option<Currency>,
|
||||||
subtotal: Option<Currency>,
|
subtotal: Option<Currency>,
|
||||||
@ -134,6 +142,8 @@ impl ServiceItemBuilder {
|
|||||||
service_id: None,
|
service_id: None,
|
||||||
product_id: None,
|
product_id: None,
|
||||||
name: None,
|
name: None,
|
||||||
|
description: None,
|
||||||
|
comments: None,
|
||||||
quantity: None,
|
quantity: None,
|
||||||
unit_price: None,
|
unit_price: None,
|
||||||
subtotal: None,
|
subtotal: None,
|
||||||
@ -168,6 +178,18 @@ impl ServiceItemBuilder {
|
|||||||
self
|
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
|
/// Set the quantity
|
||||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||||
self.quantity = Some(quantity);
|
self.quantity = Some(quantity);
|
||||||
@ -230,6 +252,8 @@ impl ServiceItemBuilder {
|
|||||||
service_id: self.service_id.ok_or("service_id is required")?,
|
service_id: self.service_id.ok_or("service_id is required")?,
|
||||||
product_id: self.product_id.ok_or("product_id is required")?,
|
product_id: self.product_id.ok_or("product_id is required")?,
|
||||||
name: self.name.ok_or("name is required")?,
|
name: self.name.ok_or("name is required")?,
|
||||||
|
description: self.description.unwrap_or_default(),
|
||||||
|
comments: self.comments.unwrap_or_default(),
|
||||||
quantity,
|
quantity,
|
||||||
unit_price,
|
unit_price,
|
||||||
subtotal,
|
subtotal,
|
||||||
|
Loading…
Reference in New Issue
Block a user