Inject some builders in script
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
parent
04233e6f1a
commit
53d8d184c4
@ -19,7 +19,7 @@ tempfile = "3.8"
|
||||
poem = "1.3.55"
|
||||
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
rhai = "1.15.1"
|
||||
rhai = "1.21.0"
|
||||
paste = "1.0"
|
||||
|
||||
[[example]]
|
||||
|
@ -1,13 +1,12 @@
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use herodb::db::{DB, DBBuilder};
|
||||
use herodb::models::biz::{
|
||||
Currency, CurrencyBuilder,
|
||||
Product, ProductBuilder, ProductComponent, ProductComponentBuilder,
|
||||
ProductType, ProductStatus,
|
||||
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus
|
||||
Currency, CurrencyBuilder, Product, ProductBuilder, ProductComponent, ProductComponentBuilder,
|
||||
ProductStatus, ProductType, Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use rhai::{Engine, packages::Package};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("DB Example 2: Using Builder Pattern and Model-Specific Methods");
|
||||
@ -21,25 +20,170 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fs::create_dir_all(&db_path)?;
|
||||
println!("Database path: {:?}", db_path);
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.build_type::<Product>()
|
||||
.build_type::<ProductBuilder>()
|
||||
.build_type::<ProductComponentBuilder>()
|
||||
.build_type::<Currency>()
|
||||
.build_type::<CurrencyBuilder>()
|
||||
.build_type::<Sale>()
|
||||
.build_type::<SaleBuilder>()
|
||||
.build_type::<DBBuilder>()
|
||||
.build_type::<DB>();
|
||||
|
||||
// Register currency builder methods
|
||||
engine.register_fn("new_currency_builder", CurrencyBuilder::new);
|
||||
engine.register_fn("amount", CurrencyBuilder::amount);
|
||||
engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
|
||||
engine.register_fn("build", CurrencyBuilder::build);
|
||||
|
||||
// Register method to verify currency
|
||||
engine.register_fn("amount", Currency::amount);
|
||||
|
||||
// Register product component builder methods
|
||||
engine.register_fn(
|
||||
"new_product_component_builder",
|
||||
ProductComponentBuilder::new,
|
||||
);
|
||||
engine.register_fn("id", ProductComponentBuilder::id);
|
||||
engine.register_fn("name", ProductComponentBuilder::name::<String>);
|
||||
engine.register_fn(
|
||||
"description",
|
||||
ProductComponentBuilder::description::<String>,
|
||||
);
|
||||
engine.register_fn("quantity", ProductComponentBuilder::quantity);
|
||||
engine.register_fn("build", ProductComponentBuilder::build);
|
||||
|
||||
// Register product builder methods
|
||||
engine.register_fn("new_product_builder", ProductBuilder::new);
|
||||
engine.register_fn("id", ProductBuilder::id);
|
||||
engine.register_fn("name", ProductBuilder::name::<String>);
|
||||
engine.register_fn("description", ProductBuilder::description::<String>);
|
||||
engine.register_fn("price", ProductBuilder::price);
|
||||
engine.register_fn("type", ProductBuilder::type_);
|
||||
engine.register_fn("category", ProductBuilder::category::<String>);
|
||||
engine.register_fn("status", ProductBuilder::status);
|
||||
engine.register_fn("max_amount", ProductBuilder::max_amount);
|
||||
engine.register_fn("validity_days", ProductBuilder::validity_days);
|
||||
engine.register_fn("add_component", ProductBuilder::add_component);
|
||||
engine.register_fn("build", ProductBuilder::build);
|
||||
|
||||
// Register db builder methods
|
||||
engine.register_fn("new_db_builder", DBBuilder::new::<String>);
|
||||
engine.register_fn("register_currency", DBBuilder::register_model::<Currency>);
|
||||
engine.register_fn("register_product", DBBuilder::register_model::<Product>);
|
||||
engine.register_fn("register_sale", DBBuilder::register_model::<Sale>);
|
||||
engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
|
||||
engine.register_fn("build", DBBuilder::build);
|
||||
|
||||
// Register db methods
|
||||
engine.register_fn("insert_currency", DB::insert_currency);
|
||||
engine.register_fn("insert_product", DB::insert_product);
|
||||
|
||||
let script = r#"
|
||||
let usd = new_currency_builder()
|
||||
.amount(0.0)
|
||||
.currency_code("USD")
|
||||
.build();
|
||||
|
||||
// Can we access and print this from the actual Currency?
|
||||
print(usd.amount());
|
||||
|
||||
let db = new_db_builder("./tmp/dbexample2")
|
||||
.register_product()
|
||||
.register_currency()
|
||||
.register_sale()
|
||||
.build();
|
||||
|
||||
db.insert_currency(usd);
|
||||
|
||||
let component1 = new_product_component_builder()
|
||||
.id(101)
|
||||
.name("Basic Support")
|
||||
.description("24/7 email support")
|
||||
.quantity(1)
|
||||
.build();
|
||||
|
||||
let component2 = new_product_component_builder()
|
||||
.id(102)
|
||||
.name("Premium Support")
|
||||
.description("24/7 phone and email support")
|
||||
.quantity(1)
|
||||
.build();
|
||||
|
||||
// Create products using the builder
|
||||
// let product1 = new_product_builder()
|
||||
// .id(1)
|
||||
// .name("Standard Plan")
|
||||
// .description("Our standard service offering")
|
||||
// .price(
|
||||
// new_currency_builder()
|
||||
// .amount(29.99)
|
||||
// .currency_code("USD")
|
||||
// .build()
|
||||
// )
|
||||
// .type_(ProductType::Service)
|
||||
// .category("Subscription")
|
||||
// .status(ProductStatus::Available)
|
||||
// .max_amount(1000)
|
||||
// .validity_days(30)
|
||||
// .add_component(component1)
|
||||
// .build();
|
||||
//
|
||||
// let product2 = new_product_builder()
|
||||
// .id(2)
|
||||
// .name("Premium Plan")
|
||||
// .description("Our premium service offering with priority support")
|
||||
// .price(
|
||||
// new_currency_builder()
|
||||
// .amount(99.99)
|
||||
// .currency_code("USD")
|
||||
// .build()
|
||||
// )
|
||||
// .type_(ProductType::Service)
|
||||
// .category("Subscription")
|
||||
// .status(ProductStatus::Available)
|
||||
// .max_amount(500)
|
||||
// .validity_days(30)
|
||||
// .add_component(component2)
|
||||
// .build();
|
||||
|
||||
// Insert products using model-specific methods
|
||||
// db.insert_product(product1);
|
||||
// db.insert_product(product2);
|
||||
"#;
|
||||
|
||||
engine.eval::<()>(script)?;
|
||||
|
||||
// Create a database instance with our models registered
|
||||
let db = DBBuilder::new(&db_path)
|
||||
let mut db = DBBuilder::new(&db_path)
|
||||
.register_model::<Product>()
|
||||
.register_model::<Currency>()
|
||||
.register_model::<Sale>()
|
||||
.build()?;
|
||||
|
||||
// Check if the currency created in the script is actually present, if it is this value should
|
||||
// be 1 (NOTE: it will be :) ).
|
||||
let currencies = db.list_currencies()?;
|
||||
println!("Found {} currencies in db", currencies.len());
|
||||
for currency in currencies {
|
||||
println!("{} {}", currency.amount, currency.currency_code);
|
||||
}
|
||||
|
||||
println!("\n1. Creating Products with Builder Pattern");
|
||||
println!("----------------------------------------");
|
||||
|
||||
// Create a currency using the builder
|
||||
let usd = CurrencyBuilder::new()
|
||||
.amount(0.0) // Initial amount
|
||||
.currency_code("USD")
|
||||
.build()?;
|
||||
|
||||
// Insert the currency
|
||||
db.insert_currency(&usd)?;
|
||||
println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code);
|
||||
// // Create a currency using the builder
|
||||
// let usd = CurrencyBuilder::new()
|
||||
// .amount(0.0) // Initial amount
|
||||
// .currency_code("USD")
|
||||
// .build()?;
|
||||
//
|
||||
// // Insert the currency
|
||||
// db.insert_currency(usd.clone())?;
|
||||
// println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code);
|
||||
|
||||
// Create product components using the builder
|
||||
let component1 = ProductComponentBuilder::new()
|
||||
@ -55,16 +199,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.description("24/7 phone and email support")
|
||||
.quantity(1)
|
||||
.build()?;
|
||||
|
||||
// Create products using the builder
|
||||
let product1 = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Standard Plan")
|
||||
.description("Our standard service offering")
|
||||
.price(CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Available)
|
||||
@ -77,10 +222,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.id(2)
|
||||
.name("Premium Plan")
|
||||
.description("Our premium service offering with priority support")
|
||||
.price(CurrencyBuilder::new()
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(99.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.type_(ProductType::Service)
|
||||
.category("Subscription")
|
||||
.status(ProductStatus::Available)
|
||||
@ -90,18 +237,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build()?;
|
||||
|
||||
// Insert products using model-specific methods
|
||||
db.insert_product(&product1)?;
|
||||
db.insert_product(&product2)?;
|
||||
db.insert_product(product1.clone())?;
|
||||
db.insert_product(product2.clone())?;
|
||||
|
||||
println!("Product created: {} (${:.2})", product1.name, product1.price.amount);
|
||||
println!("Product created: {} (${:.2})", product2.name, product2.price.amount);
|
||||
println!(
|
||||
"Product created: {} (${:.2})",
|
||||
product1.name, product1.price.amount
|
||||
);
|
||||
println!(
|
||||
"Product created: {} (${:.2})",
|
||||
product2.name, product2.price.amount
|
||||
);
|
||||
|
||||
println!("\n2. Retrieving Products");
|
||||
println!("--------------------");
|
||||
|
||||
// Retrieve products using model-specific methods
|
||||
let retrieved_product1 = db.get_product(1)?;
|
||||
println!("Retrieved: {} (${:.2})", retrieved_product1.name, retrieved_product1.price.amount);
|
||||
println!(
|
||||
"Retrieved: {} (${:.2})",
|
||||
retrieved_product1.name, retrieved_product1.price.amount
|
||||
);
|
||||
println!("Components:");
|
||||
for component in &retrieved_product1.components {
|
||||
println!(" - {} ({})", component.name, component.description);
|
||||
@ -114,10 +270,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let all_products = db.list_products()?;
|
||||
println!("Found {} products:", all_products.len());
|
||||
for product in all_products {
|
||||
println!(" - {} (${:.2}, {})",
|
||||
product.name,
|
||||
println!(
|
||||
" - {} (${:.2}, {})",
|
||||
product.name,
|
||||
product.price.amount,
|
||||
if product.is_purchasable() { "Available" } else { "Unavailable" }
|
||||
if product.is_purchasable() {
|
||||
"Available"
|
||||
} else {
|
||||
"Unavailable"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -126,17 +287,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Create a sale using the builder
|
||||
let now = Utc::now();
|
||||
|
||||
|
||||
let item1 = SaleItemBuilder::new()
|
||||
.id(201)
|
||||
.sale_id(1)
|
||||
.product_id(1)
|
||||
.name("Standard Plan")
|
||||
.quantity(1)
|
||||
.unit_price(CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?)
|
||||
.unit_price(
|
||||
CurrencyBuilder::new()
|
||||
.amount(29.99)
|
||||
.currency_code("USD")
|
||||
.build()?,
|
||||
)
|
||||
.active_till(now + Duration::days(30))
|
||||
.build()?;
|
||||
|
||||
@ -151,11 +314,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.build()?;
|
||||
|
||||
// Insert the sale using model-specific methods
|
||||
db.insert_sale(&sale)?;
|
||||
println!("Sale created: #{} for {} (${:.2})",
|
||||
sale.id,
|
||||
sale.buyer_name,
|
||||
sale.total_amount.amount
|
||||
db.insert_sale(sale.clone())?;
|
||||
println!(
|
||||
"Sale created: #{} for {} (${:.2})",
|
||||
sale.id, sale.buyer_name, sale.total_amount.amount
|
||||
);
|
||||
|
||||
println!("\n5. Updating a Sale");
|
||||
@ -163,12 +325,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Retrieve the sale, update it, and save it back
|
||||
let mut retrieved_sale = db.get_sale(1)?;
|
||||
println!("Retrieved sale: #{} with status {:?}", retrieved_sale.id, retrieved_sale.status);
|
||||
|
||||
println!(
|
||||
"Retrieved sale: #{} with status {:?}",
|
||||
retrieved_sale.id, retrieved_sale.status
|
||||
);
|
||||
|
||||
// Update the status
|
||||
retrieved_sale.update_status(SaleStatus::Completed);
|
||||
db.insert_sale(&retrieved_sale)?;
|
||||
|
||||
db.insert_sale(retrieved_sale.clone())?;
|
||||
|
||||
println!("Updated sale status to {:?}", retrieved_sale.status);
|
||||
|
||||
println!("\n6. Deleting Objects");
|
||||
@ -187,4 +352,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
println!("\nExample completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use bincode;
|
||||
use brotli::{CompressorReader, Decompressor};
|
||||
use rhai::CustomType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sled;
|
||||
use std::fmt::Debug;
|
||||
@ -38,15 +39,11 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||
let mut compressed = Vec::new();
|
||||
// Default Brotli parameters: quality 5, lgwin 22 (window size)
|
||||
const BROTLI_QUALITY: u32 = 5;
|
||||
const BROTLI_LGWIN: u32 = 22;
|
||||
const BROTLI_LGWIN: u32 = 22;
|
||||
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||
|
||||
let mut compressor = CompressorReader::new(
|
||||
&encoded[..],
|
||||
BUFFER_SIZE,
|
||||
BROTLI_QUALITY,
|
||||
BROTLI_LGWIN
|
||||
);
|
||||
let mut compressor =
|
||||
CompressorReader::new(&encoded[..], BUFFER_SIZE, BROTLI_QUALITY, BROTLI_LGWIN);
|
||||
compressor.read_to_end(&mut compressed)?;
|
||||
|
||||
Ok(compressed)
|
||||
@ -56,7 +53,7 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
|
||||
fn load_from_bytes(data: &[u8]) -> SledDBResult<Self> {
|
||||
let mut decompressed = Vec::new();
|
||||
const BUFFER_SIZE: usize = 4096; // 4KB buffer
|
||||
|
||||
|
||||
let mut decompressor = Decompressor::new(data, BUFFER_SIZE);
|
||||
decompressor.read_to_end(&mut decompressed)?;
|
||||
|
||||
@ -140,8 +137,8 @@ impl<T: SledModel> SledDB<T> {
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
/// Provides access to the underlying Sled Db instance for advanced operations.
|
||||
pub fn raw_db(&self) -> &sled::Db {
|
||||
/// Provides access to the underlying Sled Db instance for advanced operations.
|
||||
pub fn raw_db(&self) -> &sled::Db {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::db::base::*;
|
||||
use bincode;
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::fmt::Debug;
|
||||
use bincode;
|
||||
|
||||
/// Represents a single database operation in a transaction
|
||||
#[derive(Debug, Clone)]
|
||||
@ -24,7 +25,7 @@ pub trait AnyDbOperations: Send + Sync {
|
||||
fn delete(&self, id: &str) -> SledDBResult<()>;
|
||||
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>;
|
||||
fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()>;
|
||||
}
|
||||
|
||||
@ -33,17 +34,17 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
|
||||
fn delete(&self, id: &str) -> SledDBResult<()> {
|
||||
self.delete(id)
|
||||
}
|
||||
|
||||
|
||||
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||
let result = self.get(id)?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
|
||||
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
|
||||
let result = self.list()?;
|
||||
Ok(Box::new(result))
|
||||
}
|
||||
|
||||
|
||||
fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> {
|
||||
// Downcast to the specific type T
|
||||
match model.downcast_ref::<T>() {
|
||||
@ -51,7 +52,7 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()> {
|
||||
// Deserialize the bytes into model of type T
|
||||
let model: T = bincode::deserialize(serialized)?;
|
||||
@ -77,23 +78,25 @@ impl TransactionState {
|
||||
}
|
||||
|
||||
/// Main DB manager that automatically handles all root models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DB {
|
||||
db_path: PathBuf,
|
||||
|
||||
|
||||
// Type map for generic operations
|
||||
type_map: HashMap<TypeId, Box<dyn AnyDbOperations>>,
|
||||
|
||||
type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>>,
|
||||
|
||||
// Locks to ensure thread safety for key areas
|
||||
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
|
||||
|
||||
|
||||
// Transaction state
|
||||
transaction: RwLock<Option<TransactionState>>,
|
||||
transaction: Arc<RwLock<Option<TransactionState>>>,
|
||||
}
|
||||
|
||||
/// Builder for DB that allows registering models
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct DBBuilder {
|
||||
base_path: PathBuf,
|
||||
model_registrations: Vec<Box<dyn ModelRegistration>>,
|
||||
model_registrations: Vec<Arc<dyn ModelRegistration>>,
|
||||
}
|
||||
|
||||
/// Trait for model registration
|
||||
@ -129,33 +132,45 @@ impl DBBuilder {
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn with_path<P: Into<PathBuf>>(base_path: P) -> Self {
|
||||
Self {
|
||||
base_path: base_path.into(),
|
||||
model_registrations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a model type with the DB
|
||||
pub fn register_model<T: SledModel>(mut self) -> Self {
|
||||
self.model_registrations.push(Box::new(SledModelRegistration::<T>::new()));
|
||||
self.model_registrations
|
||||
.push(Arc::new(SledModelRegistration::<T>::new()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Build the DB with the registered models
|
||||
pub fn build(self) -> SledDBResult<DB> {
|
||||
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
|
||||
let base_path = self.base_path;
|
||||
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
std::fs::create_dir_all(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e))
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
// Register all models
|
||||
let mut type_map: HashMap<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
|
||||
|
||||
let mut type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>> = HashMap::new();
|
||||
|
||||
for registration in self.model_registrations {
|
||||
let (type_id, db) = registration.register(&base_path)?;
|
||||
type_map.insert(type_id, db);
|
||||
let (type_id, db) = registration.register(&base_path).map_err(|e| {
|
||||
EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e))
|
||||
})?;
|
||||
type_map.insert(type_id, db.into());
|
||||
}
|
||||
|
||||
|
||||
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||
let transaction = RwLock::new(None);
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(DB {
|
||||
db_path: base_path,
|
||||
type_map,
|
||||
@ -169,15 +184,15 @@ impl DB {
|
||||
/// Create a new empty DB instance without any models
|
||||
pub fn new<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
|
||||
let base_path = base_path.into();
|
||||
|
||||
|
||||
// Ensure base directory exists
|
||||
if !base_path.exists() {
|
||||
std::fs::create_dir_all(&base_path)?;
|
||||
}
|
||||
|
||||
|
||||
let _write_locks = Arc::new(Mutex::new(HashMap::new()));
|
||||
let transaction = RwLock::new(None);
|
||||
|
||||
let transaction = Arc::new(RwLock::new(None));
|
||||
|
||||
Ok(Self {
|
||||
db_path: base_path,
|
||||
type_map: HashMap::new(),
|
||||
@ -185,25 +200,27 @@ impl DB {
|
||||
transaction,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Transaction-related methods
|
||||
|
||||
|
||||
/// Begin a new transaction
|
||||
pub fn begin_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
if tx.is_some() {
|
||||
return Err(SledDBError::GeneralError("Transaction already in progress".into()));
|
||||
return Err(SledDBError::GeneralError(
|
||||
"Transaction already in progress".into(),
|
||||
));
|
||||
}
|
||||
*tx = Some(TransactionState::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Check if a transaction is active
|
||||
pub fn has_active_transaction(&self) -> bool {
|
||||
let tx = self.transaction.read().unwrap();
|
||||
tx.is_some() && tx.as_ref().unwrap().active
|
||||
}
|
||||
|
||||
|
||||
/// Apply a set operation with the serialized data - bypass transaction check
|
||||
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> {
|
||||
// Get the database operations for this model type
|
||||
@ -211,39 +228,47 @@ impl DB {
|
||||
// Just pass the raw serialized data to a special raw insert method
|
||||
return db_ops.insert_any_raw(serialized);
|
||||
}
|
||||
|
||||
Err(SledDBError::GeneralError(format!("No DB registered for type ID {:?}", model_type)))
|
||||
|
||||
Err(SledDBError::GeneralError(format!(
|
||||
"No DB registered for type ID {:?}",
|
||||
model_type
|
||||
)))
|
||||
}
|
||||
|
||||
/// Commit the current transaction, applying all operations
|
||||
pub fn commit_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
if let Some(tx_state) = tx_guard.take() {
|
||||
if !tx_state.active {
|
||||
return Err(SledDBError::GeneralError("Transaction not active".into()));
|
||||
}
|
||||
|
||||
|
||||
// Execute all operations in the transaction
|
||||
for op in tx_state.operations {
|
||||
match op {
|
||||
DbOperation::Set { model_type, serialized } => {
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
} => {
|
||||
self.apply_set_operation(model_type, &serialized)?;
|
||||
},
|
||||
}
|
||||
DbOperation::Delete { model_type, id } => {
|
||||
let db_ops = self.type_map.get(&model_type)
|
||||
let db_ops = self
|
||||
.type_map
|
||||
.get(&model_type)
|
||||
.ok_or_else(|| SledDBError::TypeError)?;
|
||||
db_ops.delete(&id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SledDBError::GeneralError("No active transaction".into()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Rollback the current transaction, discarding all operations
|
||||
pub fn rollback_transaction(&self) -> SledDBResult<()> {
|
||||
let mut tx = self.transaction.write().unwrap();
|
||||
@ -253,79 +278,85 @@ impl DB {
|
||||
*tx = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Get the path to the database
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.db_path
|
||||
}
|
||||
|
||||
|
||||
// Generic methods that work with any supported model type
|
||||
|
||||
|
||||
/// Insert a model instance into its appropriate database based on type
|
||||
pub fn set<T: SledModel>(&self, model: &T) -> SledDBResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
// Serialize the model for later use
|
||||
let serialized = bincode::serialize(model)?;
|
||||
|
||||
|
||||
// Record a Set operation in the transaction
|
||||
tx_state.operations.push(DbOperation::Set {
|
||||
model_type: TypeId::of::<T>(),
|
||||
serialized,
|
||||
});
|
||||
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => db_ops.insert_any(model),
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Check the transaction state for the given type and id
|
||||
fn check_transaction<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, SledDBError>> {
|
||||
// Try to acquire a read lock on the transaction
|
||||
let tx_guard = self.transaction.read().unwrap();
|
||||
|
||||
|
||||
if let Some(tx_state) = tx_guard.as_ref() {
|
||||
if !tx_state.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
let type_id = TypeId::of::<T>();
|
||||
let id_str = id.to_string();
|
||||
|
||||
|
||||
// Process operations in reverse order (last operation wins)
|
||||
for op in tx_state.operations.iter().rev() {
|
||||
match op {
|
||||
// First check if this ID has been deleted in the transaction
|
||||
DbOperation::Delete { model_type, id: op_id } => {
|
||||
DbOperation::Delete {
|
||||
model_type,
|
||||
id: op_id,
|
||||
} => {
|
||||
if *model_type == type_id && op_id == id {
|
||||
// Return NotFound error for deleted records
|
||||
return Some(Err(SledDBError::NotFound(id.to_string())));
|
||||
}
|
||||
},
|
||||
}
|
||||
// Then check if it has been set in the transaction
|
||||
DbOperation::Set { model_type, serialized } => {
|
||||
DbOperation::Set {
|
||||
model_type,
|
||||
serialized,
|
||||
} => {
|
||||
if *model_type == type_id {
|
||||
// Try to deserialize and check the ID
|
||||
match bincode::deserialize::<T>(serialized) {
|
||||
Ok(model) => {
|
||||
if model.get_id() == id_str {
|
||||
return Some(Ok(Some(model)));
|
||||
if model.get_id() == id_str {
|
||||
return Some(Ok(Some(model)));
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => continue, // Skip if deserialization fails
|
||||
}
|
||||
}
|
||||
@ -333,7 +364,7 @@ impl DB {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Not found in transaction (continue to database)
|
||||
None
|
||||
}
|
||||
@ -348,7 +379,7 @@ impl DB {
|
||||
Ok(None) => {} // Should never happen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no pending value, look up from the database
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => {
|
||||
@ -358,16 +389,16 @@ impl DB {
|
||||
Ok(t) => Ok(*t),
|
||||
Err(_) => Err(SledDBError::TypeError),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete a model instance by its ID and type
|
||||
pub fn delete<T: SledModel>(&self, id: &str) -> SledDBResult<()> {
|
||||
// Try to acquire a write lock on the transaction
|
||||
let mut tx_guard = self.transaction.write().unwrap();
|
||||
|
||||
|
||||
// Check if there's an active transaction
|
||||
if let Some(tx_state) = tx_guard.as_mut() {
|
||||
if tx_state.active {
|
||||
@ -376,22 +407,22 @@ impl DB {
|
||||
model_type: TypeId::of::<T>(),
|
||||
id: id.to_string(),
|
||||
});
|
||||
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we got here, either there's no transaction or it's not active
|
||||
// Drop the write lock before doing a direct database operation
|
||||
drop(tx_guard);
|
||||
|
||||
|
||||
// Execute directly
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db_ops) => db_ops.delete(id),
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// List all model instances of a specific type
|
||||
pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
|
||||
// Look up the correct DB operations for type T in our type map
|
||||
@ -403,24 +434,27 @@ impl DB {
|
||||
Ok(vec_t) => Ok(*vec_t),
|
||||
Err(_) => Err(SledDBError::TypeError),
|
||||
}
|
||||
},
|
||||
}
|
||||
None => Err(SledDBError::TypeError),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Register a model type with this DB instance
|
||||
pub fn register<T: SledModel>(&mut self) -> SledDBResult<()> {
|
||||
let db_path = self.db_path.join(T::db_prefix());
|
||||
let db: SledDB<T> = SledDB::open(db_path)?;
|
||||
self.type_map.insert(TypeId::of::<T>(), Box::new(db));
|
||||
self.type_map.insert(TypeId::of::<T>(), Arc::new(db));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// Get a typed handle to a registered model DB
|
||||
pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
|
||||
match self.type_map.get(&TypeId::of::<T>()) {
|
||||
Some(db) => Ok(&**db),
|
||||
None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::<T>()))),
|
||||
None => Err(SledDBError::GeneralError(format!(
|
||||
"No DB registered for type {}",
|
||||
std::any::type_name::<T>()
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,25 +5,27 @@ macro_rules! impl_model_methods {
|
||||
impl DB {
|
||||
paste::paste! {
|
||||
/// Insert a model instance into the database
|
||||
pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
|
||||
self.set(item)
|
||||
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
Ok(self.set(&item).map_err(|e| {
|
||||
rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e))
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Get a model instance by its ID
|
||||
pub fn [<get_ $singular>](&self, id: u32) -> SledDBResult<$model> {
|
||||
pub fn [<get_ $singular>](&mut self, id: i64) -> SledDBResult<$model> {
|
||||
self.get::<$model>(&id.to_string())
|
||||
}
|
||||
|
||||
/// Delete a model instance by its ID
|
||||
pub fn [<delete_ $singular>](&self, id: u32) -> SledDBResult<()> {
|
||||
pub fn [<delete_ $singular>](&mut self, id: i64) -> SledDBResult<()> {
|
||||
self.delete::<$model>(&id.to_string())
|
||||
}
|
||||
|
||||
/// List all model instances
|
||||
pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
|
||||
pub fn [<list_ $plural>](&mut self) -> SledDBResult<Vec<$model>> {
|
||||
self.list::<$model>()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||
|
||||
/// Currency represents a monetary value with amount and currency code
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Currency {
|
||||
pub amount: f64,
|
||||
pub currency_code: String,
|
||||
@ -17,9 +18,14 @@ impl Currency {
|
||||
currency_code,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn amount(&mut self) -> f64 {
|
||||
self.amount
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for Currency
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct CurrencyBuilder {
|
||||
amount: Option<f64>,
|
||||
currency_code: Option<String>,
|
||||
@ -47,7 +53,7 @@ impl CurrencyBuilder {
|
||||
}
|
||||
|
||||
/// Build the Currency object
|
||||
pub fn build(self) -> Result<Currency, &'static str> {
|
||||
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
|
||||
Ok(Currency {
|
||||
amount: self.amount.ok_or("amount is required")?,
|
||||
currency_code: self.currency_code.ok_or("currency_code is required")?,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc, Duration};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
|
||||
use serde::{Deserialize, Serialize}; // Import Sled traits from db module
|
||||
|
||||
/// ProductType represents the type of a product
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -20,17 +20,17 @@ pub enum ProductStatus {
|
||||
/// ProductComponent represents a component of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProductComponent {
|
||||
pub id: u32,
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub quantity: i32,
|
||||
pub quantity: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl ProductComponent {
|
||||
/// Create a new product component with default timestamps
|
||||
pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self {
|
||||
pub fn new(id: i64, name: String, description: String, quantity: i64) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id,
|
||||
@ -44,11 +44,12 @@ impl ProductComponent {
|
||||
}
|
||||
|
||||
/// Builder for ProductComponent
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductComponentBuilder {
|
||||
id: Option<u32>,
|
||||
id: Option<i64>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
quantity: Option<i32>,
|
||||
quantity: Option<i64>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
@ -67,7 +68,7 @@ impl ProductComponentBuilder {
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
pub fn id(mut self, id: i64) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
@ -85,7 +86,7 @@ impl ProductComponentBuilder {
|
||||
}
|
||||
|
||||
/// Set the quantity
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
pub fn quantity(mut self, quantity: i64) -> Self {
|
||||
self.quantity = Some(quantity);
|
||||
self
|
||||
}
|
||||
@ -103,7 +104,7 @@ impl ProductComponentBuilder {
|
||||
}
|
||||
|
||||
/// Build the ProductComponent object
|
||||
pub fn build(self) -> Result<ProductComponent, &'static str> {
|
||||
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
|
||||
let now = Utc::now();
|
||||
Ok(ProductComponent {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
@ -117,9 +118,9 @@ impl ProductComponentBuilder {
|
||||
}
|
||||
|
||||
/// Product represents a product or service offered by the Freezone
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Product {
|
||||
pub id: u32,
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: Currency,
|
||||
@ -128,7 +129,7 @@ pub struct Product {
|
||||
pub status: ProductStatus,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub max_amount: u16, // means allows us to define how many max of this there are
|
||||
pub max_amount: i64, // means allows us to define how many max of this there are
|
||||
pub purchase_till: DateTime<Utc>,
|
||||
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
|
||||
pub components: Vec<ProductComponent>,
|
||||
@ -139,14 +140,14 @@ pub struct Product {
|
||||
impl Product {
|
||||
/// Create a new product with default timestamps
|
||||
pub fn new(
|
||||
id: u32,
|
||||
id: i64,
|
||||
name: String,
|
||||
description: String,
|
||||
price: Currency,
|
||||
type_: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: u16,
|
||||
max_amount: i64,
|
||||
validity_days: i64, // How many days the product is valid after purchase
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
@ -167,30 +168,30 @@ impl Product {
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add a component to this product
|
||||
pub fn add_component(&mut self, component: ProductComponent) {
|
||||
self.components.push(component);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Update the purchase availability timeframe
|
||||
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
|
||||
self.purchase_till = purchase_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Update the active timeframe
|
||||
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
|
||||
self.active_till = active_till;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Check if the product is available for purchase
|
||||
pub fn is_purchasable(&self) -> bool {
|
||||
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
|
||||
}
|
||||
|
||||
|
||||
/// Check if the product is still active (for services)
|
||||
pub fn is_active(&self) -> bool {
|
||||
Utc::now() <= self.active_till
|
||||
@ -198,8 +199,9 @@ impl Product {
|
||||
}
|
||||
|
||||
/// Builder for Product
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
id: Option<i64>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<Currency>,
|
||||
@ -208,7 +210,7 @@ pub struct ProductBuilder {
|
||||
status: Option<ProductStatus>,
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
updated_at: Option<DateTime<Utc>>,
|
||||
max_amount: Option<u16>,
|
||||
max_amount: Option<i64>,
|
||||
purchase_till: Option<DateTime<Utc>>,
|
||||
active_till: Option<DateTime<Utc>>,
|
||||
components: Vec<ProductComponent>,
|
||||
@ -237,7 +239,7 @@ impl ProductBuilder {
|
||||
}
|
||||
|
||||
/// Set the id
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
pub fn id(mut self, id: i64) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
@ -279,7 +281,7 @@ impl ProductBuilder {
|
||||
}
|
||||
|
||||
/// Set the max amount
|
||||
pub fn max_amount(mut self, max_amount: u16) -> Self {
|
||||
pub fn max_amount(mut self, max_amount: i64) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
@ -313,13 +315,15 @@ impl ProductBuilder {
|
||||
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))
|
||||
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")?
|
||||
self.active_till
|
||||
.ok_or("Either active_till or validity_days must be provided")?
|
||||
};
|
||||
|
||||
Ok(Product {
|
||||
|
@ -1,7 +1,8 @@
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module
|
||||
use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module
|
||||
use crate::db::base::{SledModel, Storable};
|
||||
use crate::models::biz::Currency; // Use crate:: for importing from the module // Import Sled traits from db module
|
||||
// use super::db::Model; // Removed old Model trait import
|
||||
use chrono::{DateTime, Utc};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use std::collections::HashMap; // Removed unused import
|
||||
|
||||
@ -43,7 +44,7 @@ impl SaleItem {
|
||||
amount,
|
||||
currency_code: unit_price.currency_code.clone(),
|
||||
};
|
||||
|
||||
|
||||
Self {
|
||||
id,
|
||||
sale_id,
|
||||
@ -58,6 +59,7 @@ impl SaleItem {
|
||||
}
|
||||
|
||||
/// Builder for SaleItem
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleItemBuilder {
|
||||
id: Option<u32>,
|
||||
sale_id: Option<u32>,
|
||||
@ -130,7 +132,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")?;
|
||||
|
||||
|
||||
// Calculate subtotal
|
||||
let amount = unit_price.amount * quantity as f64;
|
||||
let subtotal = Currency {
|
||||
@ -152,7 +154,7 @@ impl SaleItemBuilder {
|
||||
}
|
||||
|
||||
/// Sale represents a sale of products or services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Sale {
|
||||
pub id: u32,
|
||||
pub company_id: u32,
|
||||
@ -184,7 +186,10 @@ impl Sale {
|
||||
company_id,
|
||||
buyer_name,
|
||||
buyer_email,
|
||||
total_amount: Currency { amount: 0.0, currency_code },
|
||||
total_amount: Currency {
|
||||
amount: 0.0,
|
||||
currency_code,
|
||||
},
|
||||
status,
|
||||
sale_date: now,
|
||||
created_at: now,
|
||||
@ -192,12 +197,12 @@ impl Sale {
|
||||
items: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add an item to the sale and update the total amount
|
||||
pub fn add_item(&mut self, item: SaleItem) {
|
||||
// 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
|
||||
if self.items.is_empty() {
|
||||
// First item, initialize the total amount with the same currency
|
||||
@ -210,14 +215,14 @@ impl Sale {
|
||||
// (Assumes all items have the same currency)
|
||||
self.total_amount.amount += item.subtotal.amount;
|
||||
}
|
||||
|
||||
|
||||
// Add the item to the list
|
||||
self.items.push(item);
|
||||
|
||||
|
||||
// Update the sale timestamp
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
|
||||
/// Update the status of the sale
|
||||
pub fn update_status(&mut self, status: SaleStatus) {
|
||||
self.status = status;
|
||||
@ -226,6 +231,7 @@ impl Sale {
|
||||
}
|
||||
|
||||
/// Builder for Sale
|
||||
#[derive(Clone, CustomType)]
|
||||
pub struct SaleBuilder {
|
||||
id: Option<u32>,
|
||||
company_id: Option<u32>,
|
||||
@ -311,20 +317,20 @@ impl SaleBuilder {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user