This commit is contained in:
2025-04-04 11:11:35 +02:00
parent 32f6d87454
commit 8a5e41a265
7 changed files with 751 additions and 57 deletions

View File

@@ -14,15 +14,23 @@ The business models are implemented as Rust structs and enums with serialization
└─────────────┘ └─────────────┘ └──────┬──────┘
▲ │
│ │
┌─────┴───────┐
┌─────┴──────────┐ │
│ProductComponent│ │
└─────────────┘
└────────────────┘ │
┌─────────────┐
│ Sale │
└─────────────┘
```
## Root Objects
- root objects are the one who are stored in the DB
- Root Objects are
- currency
- product
- Sale
## Models
### Currency (Root Object)
@@ -33,8 +41,8 @@ Represents a monetary value with an amount and currency code.
- `amount`: f64 - The monetary amount
- `currency_code`: String - The currency code (e.g., "USD", "EUR")
**Methods:**
- `new()` - Creates a new Currency instance
**Builder:**
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
### Product
@@ -59,8 +67,8 @@ Represents a component part of a product.
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Methods:**
- `new()` - Creates a new ProductComponent with default timestamps
**Builder:**
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
#### Product (Root Object)
Represents a product or service offered.
@@ -81,13 +89,15 @@ Represents a product or service offered.
- `components`: Vec<ProductComponent> - List of product components
**Methods:**
- `new()` - Creates a new Product with default timestamps
- `add_component()` - Adds a component to this product
- `set_purchase_period()` - Updates purchase availability timeframe
- `set_active_period()` - Updates active timeframe
- `is_purchasable()` - Checks if product is available for purchase
- `is_active()` - Checks if product is still active
**Builder:**
- `ProductBuilder` - Provides a fluent interface for creating Product instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
@@ -115,8 +125,8 @@ Represents an item within a sale.
- `subtotal`: Currency - Total price for this item (calculated)
- `active_till`: DateTime<Utc> - When item/service expires
**Methods:**
- `new()` - Creates a new SaleItem with calculated subtotal
**Builder:**
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
#### Sale (Root Object)
Represents a complete sale transaction.
@@ -134,10 +144,12 @@ Represents a complete sale transaction.
- `items`: Vec<SaleItem> - List of items in the sale
**Methods:**
- `new()` - Creates a new Sale with default timestamps
- `add_item()` - Adds an item to the sale and updates total
- `update_status()` - Updates the status of the sale
**Builder:**
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
@@ -146,62 +158,94 @@ Represents a complete sale transaction.
## Usage Examples
### Creating a Currency
```rust
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
```
### Creating a Product
```rust
// Create a currency
let price = Currency::new(29.99, "USD".to_string());
// Create a currency using the builder
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Create a product
let mut product = Product::new(
1, // id
"Premium Service".to_string(), // name
"Our premium service offering".to_string(), // description
price, // price
ProductType::Service, // type
"Services".to_string(), // category
ProductStatus::Available, // status
100, // max_amount
30, // validity_days (service valid for 30 days)
);
// Create a component using the builder
let component = ProductComponentBuilder::new()
.id(1)
.name("Basic Support")
.description("24/7 email support")
.quantity(1)
.build()
.expect("Failed to build product component");
// Add a component
let component = ProductComponent::new(
1, // id
"Basic Support".to_string(), // name
"24/7 email support".to_string(), // description
1, // quantity
);
product.add_component(component);
// Create a product using the builder
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(price)
.type_(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.add_component(component)
.build()
.expect("Failed to build product");
```
### Creating a Sale
```rust
// Create a new sale
let mut sale = Sale::new(
1, // id
101, // company_id
"John Doe".to_string(), // buyer_name
"john.doe@example.com".to_string(), // buyer_email
"USD".to_string(), // currency_code
SaleStatus::Pending, // status
);
// Create a sale item
let now = Utc::now();
let item = SaleItem::new(
1, // id
1, // sale_id
1, // product_id
"Premium Service".to_string(), // name
1, // quantity
Currency::new(29.99, "USD".to_string()), // unit_price
now + Duration::days(30), // active_till
);
// Add the item to the sale
sale.add_item(item);
// Create a currency using the builder
let unit_price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Complete the sale
sale.update_status(SaleStatus::Completed);
// Create a sale item using the builder
let item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.product_id(1)
.name("Premium Service")
.quantity(1)
.unit_price(unit_price)
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
// Create a sale using the builder
let mut sale = SaleBuilder::new()
.id(1)
.company_id(101)
.buyer_name("John Doe")
.buyer_email("john.doe@example.com")
.currency_code("USD")
.status(SaleStatus::Pending)
.add_item(item)
.build()
.expect("Failed to build sale");
// Update the sale status
sale.update_status(SaleStatus::Completed);
```
## Benefits of the Builder Pattern
- You don't need to remember the order of parameters.
- You get readable, self-documenting code.
- It's easier to provide defaults or optional values.
- Validation happens at build time, ensuring all required fields are provided.

View File

@@ -18,3 +18,39 @@ impl Currency {
}
}
}
/// Builder for Currency
pub struct CurrencyBuilder {
amount: Option<f64>,
currency_code: Option<String>,
}
impl CurrencyBuilder {
/// Create a new CurrencyBuilder with all fields set to None
pub fn new() -> Self {
Self {
amount: None,
currency_code: None,
}
}
/// Set the amount
pub fn amount(mut self, amount: f64) -> Self {
self.amount = Some(amount);
self
}
/// Set the currency code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Build the Currency object
pub fn build(self) -> Result<Currency, &'static str> {
Ok(Currency {
amount: self.amount.ok_or("amount is required")?,
currency_code: self.currency_code.ok_or("currency_code is required")?,
})
}
}

View File

@@ -13,10 +13,18 @@ pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use company::{Company, CompanyStatus, BusinessType};
pub use meeting::Meeting;
pub use product::{Product, Currency, ProductComponent, ProductType, ProductStatus};
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
// Re-export Currency and its builder
pub use product::Currency;
pub use currency::CurrencyBuilder;
// Re-export database components
// Re-export from core module
pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};

View File

@@ -43,6 +43,79 @@ impl ProductComponent {
}
}
/// Builder for ProductComponent
pub struct ProductComponentBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
quantity: Option<i32>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl ProductComponentBuilder {
/// Create a new ProductComponentBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
quantity: None,
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the created_at timestamp
pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
self.created_at = Some(created_at);
self
}
/// Set the updated_at timestamp
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
self.updated_at = Some(updated_at);
self
}
/// Build the ProductComponent object
pub fn build(self) -> Result<ProductComponent, &'static str> {
let now = Utc::now();
Ok(ProductComponent {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
quantity: self.quantity.ok_or("quantity is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
/// Product represents a product or service offered by the Freezone
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
@@ -124,6 +197,149 @@ impl Product {
}
}
/// Builder for Product
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<Currency>,
type_: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
max_amount: Option<u16>,
purchase_till: Option<DateTime<Utc>>,
active_till: Option<DateTime<Utc>>,
components: Vec<ProductComponent>,
validity_days: Option<i64>,
}
impl ProductBuilder {
/// Create a new ProductBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
price: None,
type_: None,
category: None,
status: None,
created_at: None,
updated_at: None,
max_amount: None,
purchase_till: None,
active_till: None,
components: Vec::new(),
validity_days: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the price
pub fn price(mut self, price: Currency) -> Self {
self.price = Some(price);
self
}
/// Set the product type
pub fn type_(mut self, type_: ProductType) -> Self {
self.type_ = Some(type_);
self
}
/// Set the category
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
/// Set the status
pub fn status(mut self, status: ProductStatus) -> Self {
self.status = Some(status);
self
}
/// Set the max amount
pub fn max_amount(mut self, max_amount: u16) -> Self {
self.max_amount = Some(max_amount);
self
}
/// Set the validity days
pub fn validity_days(mut self, validity_days: i64) -> Self {
self.validity_days = Some(validity_days);
self
}
/// Set the purchase_till date directly
pub fn purchase_till(mut self, purchase_till: DateTime<Utc>) -> Self {
self.purchase_till = Some(purchase_till);
self
}
/// Set the active_till date directly
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Add a component to the product
pub fn add_component(mut self, component: ProductComponent) -> Self {
self.components.push(component);
self
}
/// Build the Product object
pub fn build(self) -> Result<Product, &'static str> {
let now = Utc::now();
let created_at = self.created_at.unwrap_or(now);
let updated_at = self.updated_at.unwrap_or(now);
// Calculate purchase_till and active_till based on validity_days if not set directly
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
let active_till = if let Some(validity_days) = self.validity_days {
self.active_till.unwrap_or(now + Duration::days(validity_days))
} else {
self.active_till.ok_or("Either active_till or validity_days must be provided")?
};
Ok(Product {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
price: self.price.ok_or("price is required")?,
type_: self.type_.ok_or("type_ is required")?,
category: self.category.ok_or("category is required")?,
status: self.status.ok_or("status is required")?,
created_at,
updated_at,
max_amount: self.max_amount.ok_or("max_amount is required")?,
purchase_till,
active_till,
components: self.components,
})
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Product {}
@@ -137,3 +353,6 @@ impl SledModel for Product {
"product"
}
}
// Import Currency from the currency module
use super::Currency;

View File

@@ -57,6 +57,100 @@ impl SaleItem {
}
}
/// Builder for SaleItem
pub struct SaleItemBuilder {
id: Option<u32>,
sale_id: Option<u32>,
product_id: Option<u32>,
name: Option<String>,
quantity: Option<i32>,
unit_price: Option<Currency>,
subtotal: Option<Currency>,
active_till: Option<DateTime<Utc>>,
}
impl SaleItemBuilder {
/// Create a new SaleItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
sale_id: None,
product_id: None,
name: None,
quantity: None,
unit_price: None,
subtotal: None,
active_till: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self
}
/// Set the product_id
pub fn product_id(mut self, product_id: u32) -> Self {
self.product_id = Some(product_id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the unit_price
pub fn unit_price(mut self, unit_price: Currency) -> Self {
self.unit_price = Some(unit_price);
self
}
/// Set the active_till
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Build the SaleItem object
pub fn build(self) -> Result<SaleItem, &'static str> {
let unit_price = self.unit_price.ok_or("unit_price is required")?;
let quantity = self.quantity.ok_or("quantity is required")?;
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency {
amount,
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")?,
quantity,
unit_price,
subtotal,
active_till: self.active_till.ok_or("active_till is required")?,
})
}
}
/// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sale {
@@ -131,6 +225,134 @@ impl Sale {
}
}
/// Builder for Sale
pub struct SaleBuilder {
id: Option<u32>,
company_id: Option<u32>,
buyer_name: Option<String>,
buyer_email: Option<String>,
total_amount: Option<Currency>,
status: Option<SaleStatus>,
sale_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<SaleItem>,
currency_code: Option<String>,
}
impl SaleBuilder {
/// Create a new SaleBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
company_id: None,
buyer_name: None,
buyer_email: None,
total_amount: None,
status: None,
sale_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the company_id
pub fn company_id(mut self, company_id: u32) -> Self {
self.company_id = Some(company_id);
self
}
/// Set the buyer_name
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
self.buyer_name = Some(buyer_name.into());
self
}
/// Set the buyer_email
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
self.buyer_email = Some(buyer_email.into());
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: SaleStatus) -> Self {
self.status = Some(status);
self
}
/// Set the sale_date
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
self.sale_date = Some(sale_date);
self
}
/// Add an item to the sale
pub fn add_item(mut self, item: SaleItem) -> Self {
self.items.push(item);
self
}
/// Build the Sale object
pub fn build(self) -> Result<Sale, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty total amount
let mut total_amount = Currency {
amount: 0.0,
currency_code: currency_code.clone(),
};
// Calculate total amount 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;
}
}
Ok(Sale {
id,
company_id: self.company_id.ok_or("company_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")?,
total_amount: self.total_amount.unwrap_or(total_amount),
status: self.status.ok_or("status is required")?,
sale_date: self.sale_date.unwrap_or(now),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
})
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Sale {}