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

View File

@ -0,0 +1,156 @@
please refactor each of the objects in the the chosen folder to use builder paradigm, see below for an example
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
---
### ✅ Step 1: Define your struct
```rust
#[derive(Debug)]
pub enum ProductType {
Service,
// Other variants...
}
#[derive(Debug)]
pub enum ProductStatus {
Available,
Unavailable,
// Other variants...
}
#[derive(Debug)]
pub struct Product {
id: u32,
name: String,
description: String,
price: f64,
product_type: ProductType,
category: String,
status: ProductStatus,
max_amount: u32,
validity_days: u32,
}
```
---
### ✅ Step 2: Create a builder
```rust
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<f64>,
product_type: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
max_amount: Option<u32>,
validity_days: Option<u32>,
}
impl ProductBuilder {
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
price: None,
product_type: None,
category: None,
status: None,
max_amount: None,
validity_days: None,
}
}
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
pub fn price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
pub fn product_type(mut self, product_type: ProductType) -> Self {
self.product_type = Some(product_type);
self
}
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
pub fn status(mut self, status: ProductStatus) -> Self {
self.status = Some(status);
self
}
pub fn max_amount(mut self, max_amount: u32) -> Self {
self.max_amount = Some(max_amount);
self
}
pub fn validity_days(mut self, validity_days: u32) -> Self {
self.validity_days = Some(validity_days);
self
}
pub fn build(self) -> Result<Product, &'static str> {
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")?,
product_type: self.product_type.ok_or("type is required")?,
category: self.category.ok_or("category is required")?,
status: self.status.ok_or("status is required")?,
max_amount: self.max_amount.ok_or("max_amount is required")?,
validity_days: self.validity_days.ok_or("validity_days is required")?,
})
}
}
```
---
### ✅ Step 3: Use it like this
```rust
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(99.99)
.product_type(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.build()
.expect("Failed to build product");
```
---
This way:
- You dont need to remember the order of parameters.
- You get readable, self-documenting code.
- Its easier to provide defaults or optional values if you want later.
Want help generating this automatically via a macro or just want it shorter? I can show you a derive macro to do that too.

View File

@ -0,0 +1,9 @@
make a readme for the chosen folder (module)
make a dense representation of the objects and how to use them
explain what the rootobjects are
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
don't explain the low level implementation details like sled, ...

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 {}