diff --git a/herodb/aiprompts/builderparadigm.md b/herodb/aiprompts/builderparadigm.md new file mode 100644 index 0000000..f931cd1 --- /dev/null +++ b/herodb/aiprompts/builderparadigm.md @@ -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, + name: Option, + description: Option, + price: Option, + product_type: Option, + category: Option, + status: Option, + max_amount: Option, + validity_days: Option, +} + +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>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + pub fn description>(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>(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 { + 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 don’t need to remember the order of parameters. +- You get readable, self-documenting code. +- It’s 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. \ No newline at end of file diff --git a/herodb/aiprompts/moduledocu.md b/herodb/aiprompts/moduledocu.md new file mode 100644 index 0000000..4074df1 --- /dev/null +++ b/herodb/aiprompts/moduledocu.md @@ -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, ... \ No newline at end of file diff --git a/herodb/src/models/biz/README.md b/herodb/src/models/biz/README.md index e367f19..f0b2f78 100644 --- a/herodb/src/models/biz/README.md +++ b/herodb/src/models/biz/README.md @@ -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 - Creation timestamp - `updated_at`: DateTime - 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 - 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 - 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 - 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); \ No newline at end of file +// 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. \ No newline at end of file diff --git a/herodb/src/models/biz/currency.rs b/herodb/src/models/biz/currency.rs index 5215fa5..cdc7a01 100644 --- a/herodb/src/models/biz/currency.rs +++ b/herodb/src/models/biz/currency.rs @@ -18,3 +18,39 @@ impl Currency { } } } + +/// Builder for Currency +pub struct CurrencyBuilder { + amount: Option, + currency_code: Option, +} + +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>(mut self, currency_code: S) -> Self { + self.currency_code = Some(currency_code.into()); + self + } + + /// Build the Currency object + pub fn build(self) -> Result { + Ok(Currency { + amount: self.amount.ok_or("amount is required")?, + currency_code: self.currency_code.ok_or("currency_code is required")?, + }) + } +} diff --git a/herodb/src/models/biz/lib.rs b/herodb/src/models/biz/lib.rs index 0f201d8..785a06f 100644 --- a/herodb/src/models/biz/lib.rs +++ b/herodb/src/models/biz/lib.rs @@ -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}; diff --git a/herodb/src/models/biz/product.rs b/herodb/src/models/biz/product.rs index f665afc..b6ca886 100644 --- a/herodb/src/models/biz/product.rs +++ b/herodb/src/models/biz/product.rs @@ -43,6 +43,79 @@ impl ProductComponent { } } +/// Builder for ProductComponent +pub struct ProductComponentBuilder { + id: Option, + name: Option, + description: Option, + quantity: Option, + created_at: Option>, + updated_at: Option>, +} + +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>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the description + pub fn description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the 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) -> Self { + self.created_at = Some(created_at); + self + } + + /// Set the updated_at timestamp + pub fn updated_at(mut self, updated_at: DateTime) -> Self { + self.updated_at = Some(updated_at); + self + } + + /// Build the ProductComponent object + pub fn build(self) -> Result { + 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, + name: Option, + description: Option, + price: Option, + type_: Option, + category: Option, + status: Option, + created_at: Option>, + updated_at: Option>, + max_amount: Option, + purchase_till: Option>, + active_till: Option>, + components: Vec, + validity_days: Option, +} + +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>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the description + pub fn description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the 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>(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) -> Self { + self.purchase_till = Some(purchase_till); + self + } + + /// Set the active_till date directly + pub fn active_till(mut self, active_till: DateTime) -> 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 { + 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; diff --git a/herodb/src/models/biz/sale.rs b/herodb/src/models/biz/sale.rs index 06c0ce9..9e49b11 100644 --- a/herodb/src/models/biz/sale.rs +++ b/herodb/src/models/biz/sale.rs @@ -57,6 +57,100 @@ impl SaleItem { } } +/// Builder for SaleItem +pub struct SaleItemBuilder { + id: Option, + sale_id: Option, + product_id: Option, + name: Option, + quantity: Option, + unit_price: Option, + subtotal: Option, + active_till: Option>, +} + +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>(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) -> Self { + self.active_till = Some(active_till); + self + } + + /// Build the SaleItem object + pub fn build(self) -> Result { + let unit_price = self.unit_price.ok_or("unit_price is required")?; + let quantity = self.quantity.ok_or("quantity is required")?; + + // 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, + company_id: Option, + buyer_name: Option, + buyer_email: Option, + total_amount: Option, + status: Option, + sale_date: Option>, + created_at: Option>, + updated_at: Option>, + items: Vec, + currency_code: Option, +} + +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>(mut self, buyer_name: S) -> Self { + self.buyer_name = Some(buyer_name.into()); + self + } + + /// Set the buyer_email + pub fn buyer_email>(mut self, buyer_email: S) -> Self { + self.buyer_email = Some(buyer_email.into()); + self + } + + /// Set the currency_code + pub fn currency_code>(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) -> 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 { + 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 {}