...
This commit is contained in:
parent
6e9305d4e6
commit
5b5b64658c
@ -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;
|
||||
|
@ -9,27 +9,53 @@ The business models are implemented as Rust structs and enums with serialization
|
||||
## Model Relationships
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │
|
||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
▲ │
|
||||
│ │
|
||||
┌─────┴──────────┐ │
|
||||
│ProductComponent│ │
|
||||
└────────────────┘ │
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Sale │
|
||||
└─────────────┘
|
||||
│ Customer │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Currency │◄────┤ Product │◄────┤ SaleItem │◄────┤ Sale │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
▲ │
|
||||
│ │
|
||||
┌─────┴──────────┐ │
|
||||
│ProductComponent│ │
|
||||
└────────────────┘ │
|
||||
│
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ Currency │◄────┤ Service │◄────┤ ServiceItem │◄───────────┘
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ InvoiceItem │◄────┤ Invoice │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Business Logic Relationships
|
||||
|
||||
- **Customer**: The entity purchasing products or services
|
||||
- **Product/Service**: Defines what is being sold, including its base price
|
||||
- Can be marked as a template (`is_template=true`) to create copies for actual sales
|
||||
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
|
||||
- Can be linked to a Service when the sale creates an ongoing service
|
||||
- **Service**: Represents an ongoing service provided to a customer
|
||||
- Created from a Product template when the product type is Service
|
||||
- **Invoice**: Represents the billing document for a sale, with payment tracking
|
||||
- Created from a Sale object to handle billing and payment tracking
|
||||
|
||||
## Root Objects
|
||||
|
||||
- root objects are the one who are stored in the DB
|
||||
- Root Objects are
|
||||
- currency
|
||||
- product
|
||||
- Root objects are the ones stored directly in the DB
|
||||
- Root Objects are:
|
||||
- Customer
|
||||
- Currency
|
||||
- Product
|
||||
- Sale
|
||||
- Service
|
||||
- Invoice
|
||||
|
||||
## Models
|
||||
|
||||
@ -44,6 +70,23 @@ Represents a monetary value with an amount and currency code.
|
||||
**Builder:**
|
||||
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
|
||||
|
||||
### Customer (Root Object)
|
||||
|
||||
Represents a customer who can purchase products or services.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `name`: String - Customer name
|
||||
- `description`: String - Customer description
|
||||
- `pubkey`: String - Customer's public key
|
||||
- `contact_ids`: Vec<u32> - List of contact IDs
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
|
||||
**Methods:**
|
||||
- `add_contact()` - Adds a contact ID to the customer
|
||||
- `remove_contact()` - Removes a contact ID from the customer
|
||||
|
||||
### Product
|
||||
|
||||
#### ProductType Enum
|
||||
@ -70,11 +113,11 @@ Represents a component part of a product.
|
||||
**Builder:**
|
||||
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
|
||||
|
||||
#### Product (Root Object)
|
||||
#### Product (Root Object)
|
||||
Represents a product or service offered.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `id`: i64 - Unique identifier
|
||||
- `name`: String - Product name
|
||||
- `description`: String - Product description
|
||||
- `price`: Currency - Product price
|
||||
@ -83,10 +126,11 @@ Represents a product or service offered.
|
||||
- `status`: ProductStatus - Available or Unavailable
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `max_amount`: u16 - Maximum quantity available
|
||||
- `max_amount`: i64 - Maximum quantity available
|
||||
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
|
||||
- `active_till`: DateTime<Utc> - When product/service expires
|
||||
- `components`: Vec<ProductComponent> - List of product components
|
||||
- `is_template`: bool - Whether this is a template product (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_component()` - Adds a component to this product
|
||||
@ -104,7 +148,62 @@ Represents a product or service offered.
|
||||
- `get_id()` - Returns the ID as a string
|
||||
- `db_prefix()` - Returns "product" as the database prefix
|
||||
|
||||
### Sale
|
||||
### Service (Root Object)
|
||||
|
||||
#### BillingFrequency Enum
|
||||
Defines how often a service is billed:
|
||||
- `Hourly` - Billed by the hour
|
||||
- `Daily` - Billed daily
|
||||
- `Weekly` - Billed weekly
|
||||
- `Monthly` - Billed monthly
|
||||
- `Yearly` - Billed yearly
|
||||
|
||||
#### ServiceStatus Enum
|
||||
Tracks the status of a service:
|
||||
- `Active` - Service is currently active
|
||||
- `Paused` - Service is temporarily paused
|
||||
- `Cancelled` - Service has been cancelled
|
||||
- `Completed` - Service has been completed
|
||||
|
||||
#### ServiceItem
|
||||
Represents an item within a service.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `service_id`: u32 - Parent service ID
|
||||
- `product_id`: u32 - ID of the product this service is based on
|
||||
- `name`: String - Service name
|
||||
- `description`: String - Detailed description of the service item
|
||||
- `comments`: String - Additional notes or comments about the service item
|
||||
- `quantity`: i32 - Number of units
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price before tax
|
||||
- `tax_rate`: f64 - Tax rate as a percentage
|
||||
- `tax_amount`: Currency - Calculated tax amount
|
||||
- `is_taxable`: bool - Whether this item is taxable
|
||||
- `active_till`: DateTime<Utc> - When service expires
|
||||
|
||||
#### Service
|
||||
Represents an ongoing service provided to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer receiving the service
|
||||
- `total_amount`: Currency - Total service amount including tax
|
||||
- `status`: ServiceStatus - Current service status
|
||||
- `billing_frequency`: BillingFrequency - How often the service is billed
|
||||
- `service_date`: DateTime<Utc> - When service started
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<ServiceItem> - List of items in the service
|
||||
- `is_template`: bool - Whether this is a template service (to be added)
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the service and updates total
|
||||
- `calculate_total()` - Recalculates the total amount
|
||||
- `update_status()` - Updates the status of the service
|
||||
|
||||
### Sale
|
||||
|
||||
#### SaleStatus Enum
|
||||
Tracks the status of a sale:
|
||||
@ -120,11 +219,18 @@ Represents an item within a sale.
|
||||
- `sale_id`: u32 - Parent sale ID
|
||||
- `product_id`: u32 - ID of the product sold
|
||||
- `name`: String - Product name at time of sale
|
||||
- `description`: String - Detailed description of the item
|
||||
- `comments`: String - Additional notes or comments about the item
|
||||
- `quantity`: i32 - Number of items purchased
|
||||
- `unit_price`: Currency - Price per unit
|
||||
- `subtotal`: Currency - Total price for this item (calculated)
|
||||
- `subtotal`: Currency - Total price for this item before tax (calculated)
|
||||
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
- `tax_amount`: Currency - Calculated tax amount for this item
|
||||
- `active_till`: DateTime<Utc> - When item/service expires
|
||||
|
||||
**Methods:**
|
||||
- `total_with_tax()` - Returns the total amount including tax
|
||||
|
||||
**Builder:**
|
||||
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
|
||||
|
||||
@ -134,18 +240,24 @@ Represents a complete sale transaction.
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `company_id`: u32 - ID of the company making the sale
|
||||
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
|
||||
- `buyer_name`: String - Name of the buyer
|
||||
- `buyer_email`: String - Email of the buyer
|
||||
- `total_amount`: Currency - Total sale amount
|
||||
- `subtotal_amount`: Currency - Total sale amount before tax
|
||||
- `tax_amount`: Currency - Total tax amount for the sale
|
||||
- `total_amount`: Currency - Total sale amount including tax
|
||||
- `status`: SaleStatus - Current sale status
|
||||
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
|
||||
- `sale_date`: DateTime<Utc> - When sale occurred
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<SaleItem> - List of items in the sale
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the sale and updates total
|
||||
- `add_item()` - Adds an item to the sale and updates totals
|
||||
- `update_status()` - Updates the status of the sale
|
||||
- `recalculate_totals()` - Recalculates all totals based on items
|
||||
- `create_service()` - Creates a service from this sale (to be added)
|
||||
|
||||
**Builder:**
|
||||
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
|
||||
@ -223,6 +335,7 @@ let item = SaleItemBuilder::new()
|
||||
.name("Premium Service")
|
||||
.quantity(1)
|
||||
.unit_price(unit_price)
|
||||
.tax_rate(20.0) // 20% tax rate
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()
|
||||
.expect("Failed to build sale item");
|
||||
@ -241,6 +354,29 @@ let mut sale = SaleBuilder::new()
|
||||
|
||||
// Update the sale status
|
||||
sale.update_status(SaleStatus::Completed);
|
||||
|
||||
// The sale now contains:
|
||||
// - subtotal_amount: The sum of all items before tax
|
||||
// - tax_amount: The sum of all tax amounts
|
||||
// - total_amount: The total including tax
|
||||
```
|
||||
|
||||
### Relationship Between Sale and Invoice
|
||||
|
||||
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
|
||||
|
||||
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
|
||||
|
||||
```rust
|
||||
// Create an invoice item linked to a sale
|
||||
let invoice_item = InvoiceItemBuilder::new()
|
||||
.id(1)
|
||||
.invoice_id(1)
|
||||
.description("Premium Service")
|
||||
.amount(sale.total_amount.clone()) // Use the total amount from the sale
|
||||
.sale_id(sale.id) // Link to the sale
|
||||
.build()
|
||||
.expect("Failed to build invoice item");
|
||||
```
|
||||
|
||||
## Database Operations
|
||||
@ -266,4 +402,125 @@ These methods are available for all root objects:
|
||||
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
|
||||
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
|
||||
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
|
||||
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
|
||||
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
|
||||
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
|
||||
|
||||
### Invoice (Root Object)
|
||||
|
||||
#### InvoiceStatus Enum
|
||||
Tracks the status of an invoice:
|
||||
- `Draft` - Invoice is in draft state
|
||||
- `Sent` - Invoice has been sent to the customer
|
||||
- `Paid` - Invoice has been paid
|
||||
- `Overdue` - Invoice is past due date
|
||||
- `Cancelled` - Invoice has been cancelled
|
||||
|
||||
#### PaymentStatus Enum
|
||||
Tracks the payment status of an invoice:
|
||||
- `Unpaid` - Invoice has not been paid
|
||||
- `PartiallyPaid` - Invoice has been partially paid
|
||||
- `Paid` - Invoice has been fully paid
|
||||
|
||||
#### Payment
|
||||
Represents a payment made against an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `amount`: Currency - Payment amount
|
||||
- `date`: DateTime<Utc> - Payment date
|
||||
- `method`: String - Payment method
|
||||
- `comment`: String - Payment comment
|
||||
|
||||
#### InvoiceItem
|
||||
Represents an item in an invoice.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `invoice_id`: u32 - Parent invoice ID
|
||||
- `description`: String - Item description
|
||||
- `amount`: Currency - Item amount
|
||||
- `service_id`: Option<u32> - ID of the service this item is for
|
||||
- `sale_id`: Option<u32> - ID of the sale this item is for
|
||||
|
||||
**Methods:**
|
||||
- `link_to_service()` - Links the invoice item to a service
|
||||
- `link_to_sale()` - Links the invoice item to a sale
|
||||
|
||||
#### Invoice
|
||||
Represents an invoice sent to a customer.
|
||||
|
||||
**Properties:**
|
||||
- `id`: u32 - Unique identifier
|
||||
- `customer_id`: u32 - ID of the customer being invoiced
|
||||
- `total_amount`: Currency - Total invoice amount
|
||||
- `balance_due`: Currency - Amount still due
|
||||
- `status`: InvoiceStatus - Current invoice status
|
||||
- `payment_status`: PaymentStatus - Current payment status
|
||||
- `issue_date`: DateTime<Utc> - When invoice was issued
|
||||
- `due_date`: DateTime<Utc> - When payment is due
|
||||
- `created_at`: DateTime<Utc> - Creation timestamp
|
||||
- `updated_at`: DateTime<Utc> - Last update timestamp
|
||||
- `items`: Vec<InvoiceItem> - List of items in the invoice
|
||||
- `payments`: Vec<Payment> - List of payments made
|
||||
|
||||
**Methods:**
|
||||
- `add_item()` - Adds an item to the invoice
|
||||
- `calculate_total()` - Calculates the total amount
|
||||
- `add_payment()` - Adds a payment to the invoice
|
||||
- `calculate_balance()` - Calculates the balance due
|
||||
- `update_payment_status()` - Updates the payment status
|
||||
- `update_status()` - Updates the status of the invoice
|
||||
- `is_overdue()` - Checks if the invoice is overdue
|
||||
- `check_if_overdue()` - Marks the invoice as overdue if past due date
|
||||
|
||||
### Relationships Between Models
|
||||
|
||||
#### Product/Service Templates and Instances
|
||||
|
||||
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
|
||||
|
||||
#### Sale to Service Relationship
|
||||
|
||||
When a product of type `Service` is sold, a Service instance can be created from the Sale:
|
||||
|
||||
```rust
|
||||
// Create a service from a sale
|
||||
let service = sale.create_service(
|
||||
service_id,
|
||||
ServiceStatus::Active,
|
||||
BillingFrequency::Monthly
|
||||
);
|
||||
```
|
||||
|
||||
#### Sale to Invoice Relationship
|
||||
|
||||
An Invoice is created from a Sale to handle billing and payment tracking:
|
||||
|
||||
```rust
|
||||
// Create an invoice from a sale
|
||||
let invoice = Invoice::from_sale(
|
||||
invoice_id,
|
||||
sale,
|
||||
due_date
|
||||
);
|
||||
```
|
||||
|
||||
#### Customer-Centric View
|
||||
|
||||
The models allow tracking all customer interactions:
|
||||
|
||||
- What products/services they've purchased (via Sale records)
|
||||
- What ongoing services they have (via Service records)
|
||||
- What they've been invoiced for (via Invoice records)
|
||||
- What they've paid (via Payment records in Invoices)
|
||||
|
||||
```rust
|
||||
// Get all sales for a customer
|
||||
let customer_sales = db.list_sales_by_customer(customer_id);
|
||||
|
||||
// Get all services for a customer
|
||||
let customer_services = db.list_services_by_customer(customer_id);
|
||||
|
||||
// Get all invoices for a customer
|
||||
let customer_invoices = db.list_invoices_by_customer(customer_id);
|
||||
```
|
||||
|
@ -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 product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the item
|
||||
pub comments: String, // Additional comments about the item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
|
||||
pub tax_amount: Currency, // Calculated tax amount
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
}
|
||||
|
||||
@ -34,28 +38,50 @@ impl SaleItem {
|
||||
sale_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
active_till: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Calculate subtotal
|
||||
// Calculate subtotal (before tax)
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency {
|
||||
amount,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency {
|
||||
amount: tax_amount_value,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
sale_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total amount including tax
|
||||
pub fn total_with_tax(&self) -> Currency {
|
||||
Currency {
|
||||
amount: self.subtotal.amount + self.tax_amount.amount,
|
||||
currency_code: self.subtotal.currency_code.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for SaleItem
|
||||
@ -65,9 +91,13 @@ pub struct SaleItemBuilder {
|
||||
sale_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
tax_rate: Option<f64>,
|
||||
tax_amount: Option<Currency>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
@ -79,9 +109,13 @@ impl SaleItemBuilder {
|
||||
sale_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
tax_rate: None,
|
||||
tax_amount: None,
|
||||
active_till: None,
|
||||
}
|
||||
}
|
||||
@ -109,6 +143,18 @@ impl SaleItemBuilder {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
@ -122,6 +168,12 @@ impl SaleItemBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the tax_rate
|
||||
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
|
||||
self.tax_rate = Some(tax_rate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the active_till
|
||||
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
|
||||
self.active_till = Some(active_till);
|
||||
@ -132,6 +184,7 @@ impl SaleItemBuilder {
|
||||
pub fn build(self) -> Result<SaleItem, &'static str> {
|
||||
let unit_price = self.unit_price.ok_or("unit_price is required")?;
|
||||
let quantity = self.quantity.ok_or("quantity is required")?;
|
||||
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
@ -139,15 +192,26 @@ impl SaleItemBuilder {
|
||||
amount,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate tax amount
|
||||
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
|
||||
let tax_amount = Currency {
|
||||
amount: tax_amount_value,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
Ok(SaleItem {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
sale_id: self.sale_id.ok_or("sale_id is required")?,
|
||||
product_id: self.product_id.ok_or("product_id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
tax_rate,
|
||||
tax_amount,
|
||||
active_till: self.active_till.ok_or("active_till is required")?,
|
||||
})
|
||||
}
|
||||
@ -158,10 +222,14 @@ impl SaleItemBuilder {
|
||||
pub struct Sale {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
pub customer_id: u32, // ID of the customer making the purchase
|
||||
pub buyer_name: String,
|
||||
pub buyer_email: String,
|
||||
pub total_amount: Currency,
|
||||
pub subtotal_amount: Currency, // Total before tax
|
||||
pub tax_amount: Currency, // Total tax
|
||||
pub total_amount: Currency, // Total including tax
|
||||
pub status: SaleStatus,
|
||||
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
|
||||
pub sale_date: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
@ -175,22 +243,29 @@ impl Sale {
|
||||
pub fn new(
|
||||
id: u32,
|
||||
company_id: u32,
|
||||
customer_id: u32,
|
||||
buyer_name: String,
|
||||
buyer_email: String,
|
||||
currency_code: String,
|
||||
status: SaleStatus,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let zero_currency = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
company_id,
|
||||
customer_id,
|
||||
buyer_name,
|
||||
buyer_email,
|
||||
total_amount: Currency {
|
||||
amount: 0.0,
|
||||
currency_code,
|
||||
},
|
||||
subtotal_amount: zero_currency.clone(),
|
||||
tax_amount: zero_currency.clone(),
|
||||
total_amount: zero_currency,
|
||||
status,
|
||||
service_id: None,
|
||||
sale_date: now,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
@ -203,17 +278,27 @@ impl Sale {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
|
||||
|
||||
// Update the total amount
|
||||
// Update the amounts
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
self.total_amount = Currency {
|
||||
// First item, initialize the amounts with the same currency
|
||||
self.subtotal_amount = Currency {
|
||||
amount: item.subtotal.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
self.tax_amount = Currency {
|
||||
amount: item.tax_amount.amount,
|
||||
currency_code: item.tax_amount.currency_code.clone(),
|
||||
};
|
||||
self.total_amount = Currency {
|
||||
amount: item.subtotal.amount + item.tax_amount.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// Add to the existing totals
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount;
|
||||
self.subtotal_amount.amount += item.subtotal.amount;
|
||||
self.tax_amount.amount += item.tax_amount.amount;
|
||||
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Add the item to the list
|
||||
@ -222,12 +307,93 @@ impl Sale {
|
||||
// Update the sale timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Recalculate all totals based on items
|
||||
pub fn recalculate_totals(&mut self) {
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the currency code from the first item
|
||||
let currency_code = self.items[0].subtotal.currency_code.clone();
|
||||
|
||||
// Calculate the totals
|
||||
let mut subtotal = 0.0;
|
||||
let mut tax_total = 0.0;
|
||||
|
||||
for item in &self.items {
|
||||
subtotal += item.subtotal.amount;
|
||||
tax_total += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Update the amounts
|
||||
self.subtotal_amount = Currency {
|
||||
amount: subtotal,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
self.tax_amount = Currency {
|
||||
amount: tax_total,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
self.total_amount = Currency {
|
||||
amount: subtotal + tax_total,
|
||||
currency_code,
|
||||
};
|
||||
|
||||
// Update the timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Update the status of the sale
|
||||
pub fn update_status(&mut self, status: SaleStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Create a service from this sale
|
||||
/// This method should be called when a product of type Service is sold
|
||||
pub fn create_service(&mut self, service_id: u32, status: crate::models::biz::ServiceStatus, billing_frequency: crate::models::biz::BillingFrequency) -> Result<crate::models::biz::Service, &'static str> {
|
||||
use crate::models::biz::{Service, ServiceItem, ServiceStatus, BillingFrequency};
|
||||
|
||||
// Create a new service
|
||||
let mut service = Service::new(
|
||||
service_id,
|
||||
self.customer_id,
|
||||
self.total_amount.currency_code.clone(),
|
||||
status,
|
||||
billing_frequency,
|
||||
);
|
||||
|
||||
// Convert sale items to service items
|
||||
for sale_item in &self.items {
|
||||
// Check if the product is a service type
|
||||
// In a real implementation, you would check the product type from the database
|
||||
|
||||
// Create a service item from the sale item
|
||||
let service_item = ServiceItem::new(
|
||||
sale_item.id,
|
||||
service_id,
|
||||
sale_item.product_id,
|
||||
sale_item.name.clone(),
|
||||
sale_item.description.clone(), // Copy description from sale item
|
||||
sale_item.comments.clone(), // Copy comments from sale item
|
||||
sale_item.quantity,
|
||||
sale_item.unit_price.clone(),
|
||||
sale_item.tax_rate,
|
||||
true, // is_taxable
|
||||
sale_item.active_till,
|
||||
);
|
||||
|
||||
// Add the service item to the service
|
||||
service.add_item(service_item);
|
||||
}
|
||||
|
||||
// Link this sale to the service
|
||||
self.service_id = Some(service_id);
|
||||
self.updated_at = Utc::now();
|
||||
|
||||
Ok(service)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Sale
|
||||
@ -235,10 +401,14 @@ impl Sale {
|
||||
pub struct SaleBuilder {
|
||||
id: Option<u32>,
|
||||
company_id: Option<u32>,
|
||||
customer_id: Option<u32>,
|
||||
buyer_name: Option<String>,
|
||||
buyer_email: Option<String>,
|
||||
subtotal_amount: Option<Currency>,
|
||||
tax_amount: Option<Currency>,
|
||||
total_amount: Option<Currency>,
|
||||
status: Option<SaleStatus>,
|
||||
service_id: Option<u32>,
|
||||
sale_date: Option<DateTime<Utc>>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
@ -252,10 +422,14 @@ impl SaleBuilder {
|
||||
Self {
|
||||
id: None,
|
||||
company_id: None,
|
||||
customer_id: None,
|
||||
buyer_name: None,
|
||||
buyer_email: None,
|
||||
subtotal_amount: None,
|
||||
tax_amount: None,
|
||||
total_amount: None,
|
||||
status: None,
|
||||
service_id: None,
|
||||
sale_date: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@ -275,6 +449,12 @@ impl SaleBuilder {
|
||||
self.company_id = Some(company_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the customer_id
|
||||
pub fn customer_id(mut self, customer_id: u32) -> Self {
|
||||
self.customer_id = Some(customer_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the buyer_name
|
||||
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
|
||||
@ -299,6 +479,12 @@ impl SaleBuilder {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the service_id
|
||||
pub fn service_id(mut self, service_id: u32) -> Self {
|
||||
self.service_id = Some(service_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the sale_date
|
||||
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
|
||||
@ -318,39 +504,45 @@ impl SaleBuilder {
|
||||
let id = self.id.ok_or("id is required")?;
|
||||
let currency_code = self.currency_code.ok_or("currency_code is required")?;
|
||||
|
||||
// Initialize with empty total amount
|
||||
// Initialize with empty amounts
|
||||
let mut subtotal_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
let mut tax_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
let mut total_amount = Currency {
|
||||
amount: 0.0,
|
||||
currency_code: currency_code.clone(),
|
||||
};
|
||||
|
||||
// Calculate total amount from items
|
||||
// Calculate amounts from items
|
||||
for item in &self.items {
|
||||
// Make sure the item's sale_id matches this sale
|
||||
if item.sale_id != id {
|
||||
return Err("Item sale_id must match sale id");
|
||||
}
|
||||
|
||||
if total_amount.amount == 0.0 {
|
||||
// First item, initialize the total amount with the same currency
|
||||
total_amount = Currency {
|
||||
amount: item.subtotal.amount,
|
||||
currency_code: item.subtotal.currency_code.clone(),
|
||||
};
|
||||
} else {
|
||||
// Add to the existing total
|
||||
// (Assumes all items have the same currency)
|
||||
total_amount.amount += item.subtotal.amount;
|
||||
}
|
||||
subtotal_amount.amount += item.subtotal.amount;
|
||||
tax_amount.amount += item.tax_amount.amount;
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
|
||||
|
||||
Ok(Sale {
|
||||
id,
|
||||
company_id: self.company_id.ok_or("company_id is required")?,
|
||||
customer_id: self.customer_id.ok_or("customer_id is required")?,
|
||||
buyer_name: self.buyer_name.ok_or("buyer_name is required")?,
|
||||
buyer_email: self.buyer_email.ok_or("buyer_email is required")?,
|
||||
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
|
||||
tax_amount: self.tax_amount.unwrap_or(tax_amount),
|
||||
total_amount: self.total_amount.unwrap_or(total_amount),
|
||||
status: self.status.ok_or("status is required")?,
|
||||
service_id: self.service_id,
|
||||
sale_date: self.sale_date.unwrap_or(now),
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
updated_at: self.updated_at.unwrap_or(now),
|
||||
|
@ -29,6 +29,8 @@ pub struct ServiceItem {
|
||||
pub service_id: u32,
|
||||
pub product_id: u32,
|
||||
pub name: String,
|
||||
pub description: String, // Description of the service item
|
||||
pub comments: String, // Additional comments about the service item
|
||||
pub quantity: i32,
|
||||
pub unit_price: Currency,
|
||||
pub subtotal: Currency,
|
||||
@ -45,6 +47,8 @@ impl ServiceItem {
|
||||
service_id: u32,
|
||||
product_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
comments: String,
|
||||
quantity: i32,
|
||||
unit_price: Currency,
|
||||
tax_rate: f64,
|
||||
@ -76,6 +80,8 @@ impl ServiceItem {
|
||||
service_id,
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
comments,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
@ -117,6 +123,8 @@ pub struct ServiceItemBuilder {
|
||||
service_id: Option<u32>,
|
||||
product_id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
comments: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
unit_price: Option<Currency>,
|
||||
subtotal: Option<Currency>,
|
||||
@ -134,6 +142,8 @@ impl ServiceItemBuilder {
|
||||
service_id: None,
|
||||
product_id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
comments: None,
|
||||
quantity: None,
|
||||
unit_price: None,
|
||||
subtotal: None,
|
||||
@ -167,6 +177,18 @@ impl ServiceItemBuilder {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the comments
|
||||
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
|
||||
self.comments = Some(comments.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
@ -230,6 +252,8 @@ impl ServiceItemBuilder {
|
||||
service_id: self.service_id.ok_or("service_id is required")?,
|
||||
product_id: self.product_id.ok_or("product_id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.unwrap_or_default(),
|
||||
comments: self.comments.unwrap_or_default(),
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
|
Loading…
Reference in New Issue
Block a user