Merge pull request 'feat: Improve database handling of model indices' (#6) from development_fix_loading_records into main
Reviewed-on: #6
This commit is contained in:
commit
b6ec58d413
@ -83,3 +83,8 @@ required-features = ["rhai"]
|
||||
name = "biz_rhai"
|
||||
path = "examples/biz_rhai/example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
||||
[[example]]
|
||||
name = "payment_flow_rhai"
|
||||
path = "examples/biz_rhai/payment_flow_example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
@ -6,7 +6,9 @@ print("DB instance will be implicitly passed to DB functions.");
|
||||
|
||||
// --- Enum Constants ---
|
||||
print("\n--- Enum Constants ---");
|
||||
print(`CompanyStatus PendingPayment: ${CompanyStatusConstants::PendingPayment}`);
|
||||
print(`CompanyStatus Active: ${CompanyStatusConstants::Active}`);
|
||||
print(`CompanyStatus Suspended: ${CompanyStatusConstants::Suspended}`);
|
||||
print(`CompanyStatus Inactive: ${CompanyStatusConstants::Inactive}`);
|
||||
print(`BusinessType Coop: ${BusinessTypeConstants::Coop}`);
|
||||
print(`BusinessType Global: ${BusinessTypeConstants::Global}`);
|
||||
@ -28,18 +30,46 @@ let company1 = new_company(company1_name, company1_reg, company1_inc_date)
|
||||
.business_type(BusinessTypeConstants::Global)
|
||||
.industry("Technology")
|
||||
.description("Leading provider of innovative tech solutions.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
// Note: status defaults to PendingPayment for new companies
|
||||
.fiscal_year_end("12-31")
|
||||
.set_base_created_at(1672531200)
|
||||
.set_base_modified_at(1672531205);
|
||||
|
||||
print(`Company 1 Name: ${company1.name}, Status: ${company1.status}`);
|
||||
print(`Company 1 Name: ${company1.name}, Status: ${company1.status} (default for new companies)`);
|
||||
print(`Company 1 Email: ${company1.email}, Industry: ${company1.industry}`);
|
||||
// Save the company to the database
|
||||
print("\nSaving company1 to database...");
|
||||
company1 = set_company(company1); // Capture the company with the DB-assigned ID
|
||||
print("Company1 saved.");
|
||||
|
||||
// Demonstrate payment flow for the company
|
||||
print("\n--- Payment Processing for Company ---");
|
||||
print("Creating payment record for company registration...");
|
||||
let payment_intent_id = `pi_demo_${company1.id}`;
|
||||
let payment = new_payment(
|
||||
payment_intent_id,
|
||||
company1.id,
|
||||
"yearly",
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0 // Total amount (setup + 12 months)
|
||||
);
|
||||
|
||||
payment = set_payment(payment);
|
||||
print(`Payment created: ${payment.payment_intent_id}, Status: ${payment.status}`);
|
||||
|
||||
// Simulate successful payment processing
|
||||
print("Processing payment...");
|
||||
payment = payment.complete_payment(`cus_demo_${company1.id}`);
|
||||
payment = set_payment(payment);
|
||||
print(`Payment completed: ${payment.status}`);
|
||||
|
||||
// Update company status to Active after successful payment
|
||||
print("Updating company status to Active after payment...");
|
||||
company1 = company1.status(CompanyStatusConstants::Active);
|
||||
company1 = set_company(company1);
|
||||
print(`Company status updated: ${company1.status}`);
|
||||
|
||||
// Retrieve the company
|
||||
print(`\nRetrieving company by ID (${company1.id})...`);
|
||||
let retrieved_company = get_company_by_id(company1.id);
|
||||
@ -97,10 +127,11 @@ print(`Retrieved Shareholder 2: ${retrieved_sh2.name}, Type: ${retrieved_sh2.typ
|
||||
print("\n--- Testing Update for Company ---");
|
||||
let updated_company = retrieved_company
|
||||
.description("Leading global provider of cutting-edge technology solutions and services.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
// Note: Company is already Active from payment processing above
|
||||
.phone("+1-555-0199"); // Assume modified_at would be updated by set_company
|
||||
|
||||
print(`Updated Company - Name: ${updated_company.name}, New Phone: ${updated_company.phone}`);
|
||||
print(`Company Status: ${updated_company.status} (already Active from payment)`);
|
||||
set_company(updated_company);
|
||||
print("Updated Company saved.");
|
||||
|
||||
|
229
heromodels/examples/biz_rhai/payment_flow.rhai
Normal file
229
heromodels/examples/biz_rhai/payment_flow.rhai
Normal file
@ -0,0 +1,229 @@
|
||||
// Payment Flow Rhai Example
|
||||
// This script demonstrates the complete payment flow for company registration
|
||||
// using the Rhai scripting interface.
|
||||
|
||||
print("=== Payment Flow Rhai Example ===");
|
||||
print("Demonstrating company registration with payment integration");
|
||||
print("");
|
||||
|
||||
// --- Step 1: Create Company with Pending Payment Status ---
|
||||
print("Step 1: Creating company with pending payment status");
|
||||
|
||||
let company_name = "InnovateTech Solutions";
|
||||
let company_reg = "REG-ITS-2024-001";
|
||||
let company_inc_date = 1704067200; // Jan 1, 2024
|
||||
|
||||
// Create company (status defaults to PendingPayment)
|
||||
let company = new_company(company_name, company_reg, company_inc_date)
|
||||
.email("contact@innovatetech.com")
|
||||
.phone("+1-555-0199")
|
||||
.website("https://innovatetech.com")
|
||||
.address("456 Innovation Blvd, Tech Valley, TV 67890")
|
||||
.business_type(BusinessTypeConstants::Starter)
|
||||
.industry("Software Development")
|
||||
.description("Cutting-edge software solutions for modern businesses")
|
||||
.fiscal_year_end("12-31");
|
||||
|
||||
print(` Company: ${company.name}`);
|
||||
print(` Status: ${company.status} (default for new companies)`);
|
||||
print(` Registration: ${company.registration_number}`);
|
||||
print(` Email: ${company.email}`);
|
||||
|
||||
// Save company to database
|
||||
company = set_company(company);
|
||||
print(` Saved with ID: ${company.id}`);
|
||||
print("");
|
||||
|
||||
// --- Step 2: Create Payment Record ---
|
||||
print("Step 2: Creating payment record");
|
||||
|
||||
let payment_intent_id = `pi_rhai_${timestamp()}`;
|
||||
let payment_plan = "yearly";
|
||||
let setup_fee = 750.0;
|
||||
let monthly_fee = 149.0;
|
||||
let total_amount = setup_fee + (monthly_fee * 12.0); // Setup + 12 months
|
||||
|
||||
let payment = new_payment(
|
||||
payment_intent_id,
|
||||
company.id,
|
||||
payment_plan,
|
||||
setup_fee,
|
||||
monthly_fee,
|
||||
total_amount
|
||||
);
|
||||
|
||||
print(` Payment Intent ID: ${payment.payment_intent_id}`);
|
||||
print(` Company ID: ${payment.company_id}`);
|
||||
print(` Payment Plan: ${payment.payment_plan}`);
|
||||
print(` Setup Fee: $${payment.setup_fee}`);
|
||||
print(` Monthly Fee: $${payment.monthly_fee}`);
|
||||
print(` Total Amount: $${payment.total_amount}`);
|
||||
print(` Status: ${payment.status} (default for new payments)`);
|
||||
|
||||
// Save payment to database
|
||||
payment = set_payment(payment);
|
||||
print(` Saved with ID: ${payment.id}`);
|
||||
print("");
|
||||
|
||||
// --- Step 3: Process Payment Successfully ---
|
||||
print("Step 3: Processing payment...");
|
||||
|
||||
// Simulate payment processing
|
||||
print(" Contacting payment processor...");
|
||||
print(" Validating payment details...");
|
||||
print(" Processing transaction...");
|
||||
|
||||
// Complete the payment with Stripe customer ID
|
||||
let stripe_customer_id = `cus_rhai_${timestamp()}`;
|
||||
payment = payment.complete_payment(stripe_customer_id);
|
||||
|
||||
print(" Payment completed successfully!");
|
||||
print(` New Status: ${payment.status}`);
|
||||
print(` Stripe Customer ID: ${payment.stripe_customer_id}`);
|
||||
|
||||
// Save updated payment
|
||||
payment = set_payment(payment);
|
||||
print("");
|
||||
|
||||
// --- Step 4: Update Company Status to Active ---
|
||||
print("Step 4: Updating company status to Active");
|
||||
|
||||
company = company.status(CompanyStatusConstants::Active);
|
||||
print(` Company: ${company.name}`);
|
||||
print(` New Status: ${company.status}`);
|
||||
|
||||
// Save updated company
|
||||
company = set_company(company);
|
||||
print(" Company status updated successfully!");
|
||||
print("");
|
||||
|
||||
// --- Step 5: Verify Payment Status ---
|
||||
print("Step 5: Payment status verification");
|
||||
print(` Is payment completed? ${payment.is_completed()}`);
|
||||
print(` Is payment pending? ${payment.is_pending()}`);
|
||||
print(` Has payment failed? ${payment.has_failed()}`);
|
||||
print(` Is payment refunded? ${payment.is_refunded()}`);
|
||||
print("");
|
||||
|
||||
// --- Step 6: Retrieve Data from Database ---
|
||||
print("Step 6: Verifying data from database");
|
||||
|
||||
let retrieved_company = get_company_by_id(company.id);
|
||||
print(` Retrieved Company: ${retrieved_company.name}`);
|
||||
print(` Status: ${retrieved_company.status}`);
|
||||
|
||||
let retrieved_payment = get_payment_by_id(payment.id);
|
||||
print(` Retrieved Payment: ${retrieved_payment.payment_intent_id}`);
|
||||
print(` Status: ${retrieved_payment.status}`);
|
||||
print(` Total: $${retrieved_payment.total_amount}`);
|
||||
print("");
|
||||
|
||||
// --- Step 7: Demonstrate Failed Payment Scenario ---
|
||||
print("Step 7: Demonstrating failed payment scenario");
|
||||
|
||||
// Create another company for failed payment demo
|
||||
let failed_company = new_company(
|
||||
"FailureDemo Corp",
|
||||
"REG-FDC-2024-002",
|
||||
1704067200
|
||||
)
|
||||
.email("demo@failurecorp.com")
|
||||
.business_type(BusinessTypeConstants::Single)
|
||||
.industry("Consulting");
|
||||
|
||||
failed_company = set_company(failed_company);
|
||||
print(` Created company: ${failed_company.name}`);
|
||||
print(` Status: ${failed_company.status} (pending payment)`);
|
||||
|
||||
// Create payment that will fail
|
||||
let failed_payment_intent = `pi_fail_${timestamp()}`;
|
||||
let failed_payment = new_payment(
|
||||
failed_payment_intent,
|
||||
failed_company.id,
|
||||
"monthly",
|
||||
300.0,
|
||||
59.0,
|
||||
359.0
|
||||
);
|
||||
|
||||
failed_payment = set_payment(failed_payment);
|
||||
|
||||
// Simulate payment failure
|
||||
print(" Simulating payment failure...");
|
||||
failed_payment = failed_payment.fail_payment();
|
||||
failed_payment = set_payment(failed_payment);
|
||||
|
||||
print(` Failed Company: ${failed_company.name}`);
|
||||
print(` Company Status: ${failed_company.status} (remains pending)`);
|
||||
print(` Payment Status: ${failed_payment.status}`);
|
||||
print(` Payment failed: ${failed_payment.has_failed()}`);
|
||||
print("");
|
||||
|
||||
// --- Step 8: Demonstrate Payment Refund ---
|
||||
print("Step 8: Demonstrating payment refund scenario");
|
||||
|
||||
// Create a payment to refund
|
||||
let refund_company = new_company(
|
||||
"RefundDemo Inc",
|
||||
"REG-RDI-2024-003",
|
||||
1704067200
|
||||
)
|
||||
.email("refund@demo.com")
|
||||
.business_type(BusinessTypeConstants::Twin);
|
||||
|
||||
refund_company = set_company(refund_company);
|
||||
|
||||
let refund_payment = new_payment(
|
||||
`pi_refund_${timestamp()}`,
|
||||
refund_company.id,
|
||||
"monthly",
|
||||
200.0,
|
||||
39.0,
|
||||
239.0
|
||||
);
|
||||
|
||||
refund_payment = set_payment(refund_payment);
|
||||
|
||||
// First complete the payment
|
||||
refund_payment = refund_payment.complete_payment(`cus_refund_${timestamp()}`);
|
||||
refund_payment = set_payment(refund_payment);
|
||||
print(` Payment completed: ${refund_payment.is_completed()}`);
|
||||
|
||||
// Then refund it
|
||||
refund_payment = refund_payment.refund_payment();
|
||||
refund_payment = set_payment(refund_payment);
|
||||
|
||||
print(` Payment refunded: ${refund_payment.is_refunded()}`);
|
||||
print(` Refund Status: ${refund_payment.status}`);
|
||||
print("");
|
||||
|
||||
// --- Summary ---
|
||||
print("=== Payment Flow Example Complete ===");
|
||||
print("Summary of demonstrated features:");
|
||||
print("✓ Company creation with PendingPayment status (default)");
|
||||
print("✓ Payment record creation and database persistence");
|
||||
print("✓ Successful payment processing and status updates");
|
||||
print("✓ Company status transition from PendingPayment to Active");
|
||||
print("✓ Payment status verification methods");
|
||||
print("✓ Database retrieval and verification");
|
||||
print("✓ Failed payment scenario handling");
|
||||
print("✓ Payment refund processing");
|
||||
print("");
|
||||
print("Key Payment Statuses:");
|
||||
print("- Pending: Initial state for new payments");
|
||||
print("- Completed: Payment successfully processed");
|
||||
print("- Failed: Payment processing failed");
|
||||
print("- Refunded: Previously completed payment was refunded");
|
||||
print("");
|
||||
print("Key Company Statuses:");
|
||||
print("- PendingPayment: Default for new companies (awaiting payment)");
|
||||
print("- Active: Payment completed, company is operational");
|
||||
print("- Suspended: Company temporarily suspended");
|
||||
print("- Inactive: Company deactivated");
|
||||
|
||||
// Helper function to get current timestamp
|
||||
fn timestamp() {
|
||||
// This would normally return current timestamp
|
||||
// For demo purposes, we'll use a static value
|
||||
1704067200
|
||||
}
|
220
heromodels/examples/biz_rhai/payment_flow_example.rs
Normal file
220
heromodels/examples/biz_rhai/payment_flow_example.rs
Normal file
@ -0,0 +1,220 @@
|
||||
// Payment Flow Rhai Example Runner
|
||||
// This example runs the payment_flow.rhai script to demonstrate
|
||||
// the payment integration using Rhai scripting.
|
||||
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::biz::register_biz_rhai_module;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Payment Flow Rhai Example Runner ===");
|
||||
println!("Running payment flow demonstration using Rhai scripting\n");
|
||||
|
||||
// Initialize database
|
||||
let db = Arc::new(
|
||||
OurDB::new("/tmp/payment_flow_rhai_example", true)
|
||||
.map_err(|e| format!("DB Error: {}", e))?,
|
||||
);
|
||||
|
||||
// Create and configure Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the business models module with the engine
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Add a timestamp function for the Rhai script
|
||||
engine.register_fn("timestamp", || -> i64 { chrono::Utc::now().timestamp() });
|
||||
|
||||
// Read and execute the Rhai script
|
||||
let script_path = "examples/biz_rhai/payment_flow.rhai";
|
||||
|
||||
match std::fs::read_to_string(script_path) {
|
||||
Ok(script_content) => {
|
||||
println!("Executing Rhai script: {}\n", script_path);
|
||||
|
||||
match engine.eval::<()>(&script_content) {
|
||||
Ok(_) => {
|
||||
println!("\n✅ Rhai script executed successfully!");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error executing Rhai script: {}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error reading script file {}: {}", script_path, e);
|
||||
println!("Note: Make sure to run this example from the project root directory.");
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("The payment flow has been successfully demonstrated using Rhai scripting.");
|
||||
println!("This shows how the payment integration can be used in scripted environments.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use heromodels::db::Collection;
|
||||
use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus};
|
||||
|
||||
#[test]
|
||||
fn test_rhai_payment_integration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test that we can create and manipulate payment objects through Rhai
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_rhai_payment".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Test creating a company through Rhai
|
||||
let company_script = r#"
|
||||
let company = new_company("Test Company", "TEST-001", 1704067200)
|
||||
.email("test@example.com")
|
||||
.status(CompanyStatusConstants::PendingPayment);
|
||||
company = set_company(company);
|
||||
company.id
|
||||
"#;
|
||||
|
||||
let company_id: i64 = engine.eval(company_script)?;
|
||||
assert!(company_id > 0);
|
||||
|
||||
// Test creating a payment through Rhai
|
||||
let payment_script = format!(
|
||||
r#"
|
||||
let payment = new_payment("pi_test_123", {}, "monthly", 100.0, 50.0, 150.0);
|
||||
payment = set_payment(payment);
|
||||
payment.id
|
||||
"#,
|
||||
company_id
|
||||
);
|
||||
|
||||
let payment_id: i64 = engine.eval(&payment_script)?;
|
||||
assert!(payment_id > 0);
|
||||
|
||||
// Test completing payment through Rhai
|
||||
let complete_script = format!(
|
||||
r#"
|
||||
let payment = get_payment_by_id({});
|
||||
payment = payment.complete_payment("cus_test_123");
|
||||
payment = set_payment(payment);
|
||||
payment.is_completed()
|
||||
"#,
|
||||
payment_id
|
||||
);
|
||||
|
||||
let is_completed: bool = engine.eval(&complete_script)?;
|
||||
assert!(is_completed);
|
||||
|
||||
// Verify in database
|
||||
let payment: Payment = db.get_by_id(payment_id as u32)?.unwrap();
|
||||
assert_eq!(payment.status, PaymentStatus::Completed);
|
||||
assert_eq!(payment.stripe_customer_id, Some("cus_test_123".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_constants() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test that payment status constants are available in Rhai
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_payment_constants".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Test that we can access payment status constants
|
||||
let constants_script = r#"
|
||||
let payment = new_payment("pi_test", 1, "monthly", 100.0, 50.0, 150.0);
|
||||
|
||||
// Test status transitions
|
||||
payment = payment.status(PaymentStatusConstants::Pending);
|
||||
let is_pending = payment.is_pending();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Completed);
|
||||
let is_completed = payment.is_completed();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Failed);
|
||||
let has_failed = payment.has_failed();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Refunded);
|
||||
let is_refunded = payment.is_refunded();
|
||||
|
||||
[is_pending, is_completed, has_failed, is_refunded]
|
||||
"#;
|
||||
|
||||
let results: Vec<bool> = engine.eval(constants_script)?;
|
||||
assert_eq!(results, vec![true, true, true, true]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_company_status_integration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test the integration between company and payment status
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_status_integration".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
let integration_script = r#"
|
||||
// Create company (defaults to PendingPayment)
|
||||
let company = new_company("Integration Test", "INT-001", 1704067200);
|
||||
company = set_company(company);
|
||||
|
||||
// Create payment
|
||||
let payment = new_payment("pi_int_test", company.id, "yearly", 500.0, 99.0, 1688.0);
|
||||
payment = set_payment(payment);
|
||||
|
||||
// Complete payment
|
||||
payment = payment.complete_payment("cus_int_test");
|
||||
payment = set_payment(payment);
|
||||
|
||||
// Update company to active
|
||||
company = company.status(CompanyStatusConstants::Active);
|
||||
company = set_company(company);
|
||||
|
||||
[payment.is_completed(), company.status]
|
||||
"#;
|
||||
|
||||
let results: Vec<rhai::Dynamic> = engine.eval(integration_script)?;
|
||||
|
||||
// Check that payment is completed
|
||||
assert!(results[0].as_bool().unwrap());
|
||||
|
||||
// Check that company status is Active (we can't directly compare enum in Rhai result)
|
||||
// So we'll verify by retrieving from database
|
||||
let companies: Vec<Company> = db.get_all()?;
|
||||
let company = companies
|
||||
.into_iter()
|
||||
.find(|c| c.name == "Integration Test")
|
||||
.unwrap();
|
||||
assert_eq!(company.status, CompanyStatus::Active);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event};
|
||||
use heromodels::models::User;
|
||||
use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus};
|
||||
use heromodels_core::Model;
|
||||
|
||||
fn main() {
|
||||
@ -11,122 +12,636 @@ fn main() {
|
||||
println!("Hero Models - Calendar Usage Example");
|
||||
println!("====================================");
|
||||
|
||||
// --- Create Attendees ---
|
||||
let attendee1 = Attendee::new("user_123".to_string())
|
||||
.status(AttendanceStatus::Accepted);
|
||||
let attendee2 = Attendee::new("user_456".to_string())
|
||||
.status(AttendanceStatus::Tentative);
|
||||
let attendee3 = Attendee::new("user_789".to_string()); // Default NoResponse
|
||||
// --- Create Users First ---
|
||||
println!("\n--- Creating Users ---");
|
||||
let user1 = User::new()
|
||||
.username("alice_johnson")
|
||||
.email("alice.johnson@company.com")
|
||||
.full_name("Alice Johnson")
|
||||
.is_active(true)
|
||||
.build();
|
||||
|
||||
// --- Create Events ---
|
||||
let user2 = User::new()
|
||||
.username("bob_smith")
|
||||
.email("bob.smith@company.com")
|
||||
.full_name("Bob Smith")
|
||||
.is_active(true)
|
||||
.build();
|
||||
|
||||
let user3 = User::new()
|
||||
.username("carol_davis")
|
||||
.email("carol.davis@company.com")
|
||||
.full_name("Carol Davis")
|
||||
.is_active(true)
|
||||
.build();
|
||||
|
||||
// Store users in database and get their IDs
|
||||
let user_collection = db.collection::<User>().expect("can open user collection");
|
||||
|
||||
let (user1_id, stored_user1) = user_collection.set(&user1).expect("can set user1");
|
||||
let (user2_id, stored_user2) = user_collection.set(&user2).expect("can set user2");
|
||||
let (user3_id, stored_user3) = user_collection.set(&user3).expect("can set user3");
|
||||
|
||||
println!("Created users:");
|
||||
println!("- User 1 (ID: {}): {}", user1_id, stored_user1.full_name);
|
||||
println!("- User 2 (ID: {}): {}", user2_id, stored_user2.full_name);
|
||||
println!("- User 3 (ID: {}): {}", user3_id, stored_user3.full_name);
|
||||
|
||||
// --- Create Attendees ---
|
||||
println!("\n--- Creating Attendees ---");
|
||||
let attendee1 = Attendee::new(user1_id).status(AttendanceStatus::Accepted);
|
||||
let attendee2 = Attendee::new(user2_id).status(AttendanceStatus::Tentative);
|
||||
let attendee3 = Attendee::new(user3_id); // Default NoResponse
|
||||
|
||||
// Store attendees in database and get their IDs
|
||||
let attendee_collection = db
|
||||
.collection::<Attendee>()
|
||||
.expect("can open attendee collection");
|
||||
|
||||
let (attendee1_id, stored_attendee1) = attendee_collection
|
||||
.set(&attendee1)
|
||||
.expect("can set attendee1");
|
||||
let (attendee2_id, stored_attendee2) = attendee_collection
|
||||
.set(&attendee2)
|
||||
.expect("can set attendee2");
|
||||
let (attendee3_id, stored_attendee3) = attendee_collection
|
||||
.set(&attendee3)
|
||||
.expect("can set attendee3");
|
||||
|
||||
println!("Created attendees:");
|
||||
println!(
|
||||
"- Attendee 1 (ID: {}): Contact ID {}, Status: {:?}",
|
||||
attendee1_id, stored_attendee1.contact_id, stored_attendee1.status
|
||||
);
|
||||
println!(
|
||||
"- Attendee 2 (ID: {}): Contact ID {}, Status: {:?}",
|
||||
attendee2_id, stored_attendee2.contact_id, stored_attendee2.status
|
||||
);
|
||||
println!(
|
||||
"- Attendee 3 (ID: {}): Contact ID {}, Status: {:?}",
|
||||
attendee3_id, stored_attendee3.contact_id, stored_attendee3.status
|
||||
);
|
||||
|
||||
// --- Create Events with Attendees ---
|
||||
println!("\n--- Creating Events with Enhanced Features ---");
|
||||
let now = Utc::now();
|
||||
|
||||
let event1 = Event::new(
|
||||
"event_alpha".to_string(),
|
||||
"Team Meeting",
|
||||
now + Duration::seconds(3600), // Using Duration::seconds for more explicit chrono 0.4 compatibility
|
||||
now + Duration::seconds(7200),
|
||||
now + Duration::hours(1),
|
||||
now + Duration::hours(2),
|
||||
)
|
||||
.description("Weekly sync-up meeting.")
|
||||
.description("Weekly sync-up meeting to discuss project progress.")
|
||||
.location("Conference Room A")
|
||||
.add_attendee(attendee1.clone())
|
||||
.add_attendee(attendee2.clone());
|
||||
.color("#FF5722") // Red-orange color
|
||||
.created_by(user1_id)
|
||||
.status(EventStatus::Published)
|
||||
.category("Work")
|
||||
.reminder_minutes(15)
|
||||
.timezone("UTC")
|
||||
.add_attendee(attendee1_id)
|
||||
.add_attendee(attendee2_id);
|
||||
|
||||
let event2 = Event::new(
|
||||
"event_beta".to_string(),
|
||||
"Project Brainstorm",
|
||||
now + Duration::days(1),
|
||||
now + Duration::days(1) + Duration::seconds(5400), // 90 minutes
|
||||
now + Duration::days(1) + Duration::minutes(90),
|
||||
)
|
||||
.description("Brainstorming session for new project features.")
|
||||
.add_attendee(attendee1.clone())
|
||||
.add_attendee(attendee3.clone());
|
||||
.location("Innovation Lab")
|
||||
.color("#4CAF50") // Green color
|
||||
.created_by(user2_id)
|
||||
.status(EventStatus::Draft)
|
||||
.category("Planning")
|
||||
.reminder_minutes(30)
|
||||
.is_recurring(true)
|
||||
.add_attendee(attendee1_id)
|
||||
.add_attendee(attendee3_id);
|
||||
|
||||
let event3_for_calendar2 = Event::new(
|
||||
"event_gamma".to_string(),
|
||||
let event3 = Event::new(
|
||||
"Client Call",
|
||||
now + Duration::days(2),
|
||||
now + Duration::days(2) + Duration::seconds(3600)
|
||||
now + Duration::days(2) + Duration::hours(1),
|
||||
)
|
||||
.description("Quarterly review with key client.")
|
||||
.color("#9C27B0") // Purple color
|
||||
.created_by(user3_id)
|
||||
.status(EventStatus::Published)
|
||||
.category("Client")
|
||||
.reminder_minutes(60)
|
||||
.add_attendee(attendee2_id);
|
||||
|
||||
// Create an all-day event
|
||||
let event4 = Event::new(
|
||||
"Company Holiday",
|
||||
now + Duration::days(7),
|
||||
now + Duration::days(7) + Duration::hours(24),
|
||||
)
|
||||
.description("National holiday - office closed.")
|
||||
.color("#FFC107") // Amber color
|
||||
.all_day(true)
|
||||
.created_by(user1_id)
|
||||
.status(EventStatus::Published)
|
||||
.category("Holiday");
|
||||
|
||||
println!("Created events with enhanced features:");
|
||||
println!(
|
||||
"- Event 1: '{}' at {} with {} attendees",
|
||||
event1.title,
|
||||
event1.start_time.format("%Y-%m-%d %H:%M"),
|
||||
event1.attendees.len()
|
||||
);
|
||||
println!(
|
||||
" Location: {}",
|
||||
event1
|
||||
.location
|
||||
.as_ref()
|
||||
.unwrap_or(&"Not specified".to_string())
|
||||
);
|
||||
println!(
|
||||
" Color: {}",
|
||||
event1.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(
|
||||
" Category: {}",
|
||||
event1.category.as_ref().unwrap_or(&"None".to_string())
|
||||
);
|
||||
println!(" Status: {:?}", event1.status);
|
||||
println!(" Created by: User ID {}", event1.created_by.unwrap_or(0));
|
||||
println!(
|
||||
" Reminder: {} minutes before",
|
||||
event1.reminder_minutes.unwrap_or(0)
|
||||
);
|
||||
println!(" All-day: {}", event1.all_day);
|
||||
println!(" Recurring: {}", event1.is_recurring);
|
||||
println!(" Attendee IDs: {:?}", event1.attendees);
|
||||
|
||||
println!(
|
||||
"- Event 2: '{}' at {} with {} attendees",
|
||||
event2.title,
|
||||
event2.start_time.format("%Y-%m-%d %H:%M"),
|
||||
event2.attendees.len()
|
||||
);
|
||||
println!(
|
||||
" Location: {}",
|
||||
event2
|
||||
.location
|
||||
.as_ref()
|
||||
.unwrap_or(&"Not specified".to_string())
|
||||
);
|
||||
println!(
|
||||
" Color: {}",
|
||||
event2.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(
|
||||
" Category: {}",
|
||||
event2.category.as_ref().unwrap_or(&"None".to_string())
|
||||
);
|
||||
println!(" Status: {:?}", event2.status);
|
||||
println!(" Created by: User ID {}", event2.created_by.unwrap_or(0));
|
||||
println!(
|
||||
" Reminder: {} minutes before",
|
||||
event2.reminder_minutes.unwrap_or(0)
|
||||
);
|
||||
println!(" All-day: {}", event2.all_day);
|
||||
println!(" Recurring: {}", event2.is_recurring);
|
||||
println!(" Attendee IDs: {:?}", event2.attendees);
|
||||
|
||||
println!(
|
||||
"- Event 3: '{}' at {} with {} attendees",
|
||||
event3.title,
|
||||
event3.start_time.format("%Y-%m-%d %H:%M"),
|
||||
event3.attendees.len()
|
||||
);
|
||||
println!(
|
||||
" Location: {}",
|
||||
event3
|
||||
.location
|
||||
.as_ref()
|
||||
.unwrap_or(&"Not specified".to_string())
|
||||
);
|
||||
println!(
|
||||
" Color: {}",
|
||||
event3.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(
|
||||
" Category: {}",
|
||||
event3.category.as_ref().unwrap_or(&"None".to_string())
|
||||
);
|
||||
println!(" Status: {:?}", event3.status);
|
||||
println!(" Created by: User ID {}", event3.created_by.unwrap_or(0));
|
||||
println!(
|
||||
" Reminder: {} minutes before",
|
||||
event3.reminder_minutes.unwrap_or(0)
|
||||
);
|
||||
println!(" All-day: {}", event3.all_day);
|
||||
println!(" Recurring: {}", event3.is_recurring);
|
||||
println!(" Attendee IDs: {:?}", event3.attendees);
|
||||
|
||||
println!(
|
||||
"- Event 4: '{}' at {} (All-day: {})",
|
||||
event4.title,
|
||||
event4.start_time.format("%Y-%m-%d"),
|
||||
event4.all_day
|
||||
);
|
||||
println!(
|
||||
" Color: {}",
|
||||
event4.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(
|
||||
" Category: {}",
|
||||
event4.category.as_ref().unwrap_or(&"None".to_string())
|
||||
);
|
||||
println!(" Status: {:?}", event4.status);
|
||||
println!(" Created by: User ID {}", event4.created_by.unwrap_or(0));
|
||||
|
||||
// --- Demonstrate Event Manipulation ---
|
||||
println!("\n--- Demonstrating Event Manipulation ---");
|
||||
|
||||
// Reschedule an event
|
||||
let new_start = now + Duration::hours(2);
|
||||
let new_end = now + Duration::hours(3);
|
||||
let mut updated_event1 = event1.clone();
|
||||
updated_event1 = updated_event1.reschedule(new_start, new_end);
|
||||
println!(
|
||||
"Rescheduled '{}' to {}",
|
||||
updated_event1.title,
|
||||
new_start.format("%Y-%m-%d %H:%M")
|
||||
);
|
||||
|
||||
// Remove an attendee
|
||||
updated_event1 = updated_event1.remove_attendee(attendee1_id);
|
||||
println!(
|
||||
"Removed attendee {} from '{}'. Remaining attendee IDs: {:?}",
|
||||
attendee1_id, updated_event1.title, updated_event1.attendees
|
||||
);
|
||||
|
||||
// Add a new attendee
|
||||
updated_event1 = updated_event1.add_attendee(attendee3_id);
|
||||
println!(
|
||||
"Added attendee {} to '{}'. Current attendee IDs: {:?}",
|
||||
attendee3_id, updated_event1.title, updated_event1.attendees
|
||||
);
|
||||
|
||||
// --- Demonstrate Event Status Changes ---
|
||||
println!("\n--- Demonstrating Event Status Changes ---");
|
||||
|
||||
// Change event status from Draft to Published
|
||||
let mut updated_event2 = event2.clone();
|
||||
updated_event2 = updated_event2.status(EventStatus::Published);
|
||||
println!(
|
||||
"Changed '{}' status from Draft to Published",
|
||||
updated_event2.title
|
||||
);
|
||||
|
||||
// Cancel an event
|
||||
let mut cancelled_event = event3.clone();
|
||||
cancelled_event = cancelled_event.status(EventStatus::Cancelled);
|
||||
println!("Cancelled event: '{}'", cancelled_event.title);
|
||||
|
||||
// Update event with new features
|
||||
let enhanced_event = Event::new(
|
||||
"Enhanced Meeting",
|
||||
now + Duration::days(5),
|
||||
now + Duration::days(5) + Duration::hours(2),
|
||||
)
|
||||
.description("Meeting with all new features demonstrated.")
|
||||
.location("Virtual - Zoom")
|
||||
.color("#673AB7") // Deep purple
|
||||
.created_by(user1_id)
|
||||
.status(EventStatus::Published)
|
||||
.category("Demo")
|
||||
.reminder_minutes(45)
|
||||
.timezone("America/New_York")
|
||||
.is_recurring(true)
|
||||
.add_attendee(attendee1_id)
|
||||
.add_attendee(attendee2_id)
|
||||
.add_attendee(attendee3_id);
|
||||
|
||||
println!("Created enhanced event with all features:");
|
||||
println!(" Title: {}", enhanced_event.title);
|
||||
println!(" Status: {:?}", enhanced_event.status);
|
||||
println!(" Category: {}", enhanced_event.category.as_ref().unwrap());
|
||||
println!(" Color: {}", enhanced_event.color.as_ref().unwrap());
|
||||
println!(" Timezone: {}", enhanced_event.timezone.as_ref().unwrap());
|
||||
println!(" Recurring: {}", enhanced_event.is_recurring);
|
||||
println!(
|
||||
" Reminder: {} minutes",
|
||||
enhanced_event.reminder_minutes.unwrap()
|
||||
);
|
||||
println!(" Attendees: {} people", enhanced_event.attendees.len());
|
||||
|
||||
// --- Store Events in Database ---
|
||||
// Now that Event is a proper database model, we need to store events first
|
||||
println!("\n--- Storing Events in Database ---");
|
||||
|
||||
let event_collection = db.collection::<Event>().expect("can open event collection");
|
||||
|
||||
// Store events and get their auto-generated IDs
|
||||
let (event1_id, stored_event1) = event_collection.set(&event1).expect("can set event1");
|
||||
let (event2_id, stored_event2) = event_collection.set(&event2).expect("can set event2");
|
||||
let (event3_id, stored_event3) = event_collection.set(&event3).expect("can set event3");
|
||||
let (event4_id, stored_event4) = event_collection.set(&event4).expect("can set event4");
|
||||
|
||||
println!("Stored events in database:");
|
||||
println!(
|
||||
"- Event ID {}: '{}' (Status: {:?})",
|
||||
event1_id, stored_event1.title, stored_event1.status
|
||||
);
|
||||
println!(
|
||||
"- Event ID {}: '{}' (Status: {:?})",
|
||||
event2_id, stored_event2.title, stored_event2.status
|
||||
);
|
||||
println!(
|
||||
"- Event ID {}: '{}' (Status: {:?})",
|
||||
event3_id, stored_event3.title, stored_event3.status
|
||||
);
|
||||
println!(
|
||||
"- Event ID {}: '{}' (All-day: {})",
|
||||
event4_id, stored_event4.title, stored_event4.all_day
|
||||
);
|
||||
|
||||
// --- Create Calendars ---
|
||||
// Note: Calendar::new directly returns Calendar, no separate .build() step like the user example.
|
||||
// Now calendars store the actual database IDs of the events
|
||||
println!("\n--- Creating Enhanced Calendars ---");
|
||||
|
||||
// Create a calendar with auto-generated ID
|
||||
// Create a work calendar with enhanced features
|
||||
let calendar1 = Calendar::new(None, "Work Calendar")
|
||||
.description("Calendar for all work-related events.")
|
||||
.add_event(event1.clone())
|
||||
.add_event(event2.clone());
|
||||
.owner_id(user1_id)
|
||||
.is_public(false)
|
||||
.color("#2196F3") // Blue color
|
||||
.add_event(event1_id as i64)
|
||||
.add_event(event2_id as i64);
|
||||
|
||||
// Create a calendar with auto-generated ID (explicit IDs are no longer supported)
|
||||
// Create a personal calendar
|
||||
let calendar2 = Calendar::new(None, "Personal Calendar")
|
||||
.add_event(event3_for_calendar2.clone());
|
||||
.description("Personal events and appointments.")
|
||||
.owner_id(user2_id)
|
||||
.is_public(false)
|
||||
.color("#E91E63") // Pink color
|
||||
.add_event(event3_id as i64);
|
||||
|
||||
// Create a company-wide calendar
|
||||
let calendar3 = Calendar::new(None, "Company Events")
|
||||
.description("Company-wide events and holidays.")
|
||||
.owner_id(user1_id)
|
||||
.is_public(true)
|
||||
.color("#FF9800") // Orange color
|
||||
.add_event(event4_id as i64);
|
||||
|
||||
println!("Created calendars with enhanced features:");
|
||||
println!(
|
||||
"- Calendar 1: '{}' with {} events",
|
||||
calendar1.name,
|
||||
calendar1.events.len()
|
||||
);
|
||||
println!(" Owner: User ID {}", calendar1.owner_id.unwrap_or(0));
|
||||
println!(" Public: {}", calendar1.is_public);
|
||||
println!(
|
||||
" Color: {}",
|
||||
calendar1.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(" Events: {:?}", calendar1.events);
|
||||
|
||||
println!(
|
||||
"- Calendar 2: '{}' with {} events",
|
||||
calendar2.name,
|
||||
calendar2.events.len()
|
||||
);
|
||||
println!(" Owner: User ID {}", calendar2.owner_id.unwrap_or(0));
|
||||
println!(" Public: {}", calendar2.is_public);
|
||||
println!(
|
||||
" Color: {}",
|
||||
calendar2.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(" Events: {:?}", calendar2.events);
|
||||
|
||||
println!(
|
||||
"- Calendar 3: '{}' with {} events",
|
||||
calendar3.name,
|
||||
calendar3.events.len()
|
||||
);
|
||||
println!(" Owner: User ID {}", calendar3.owner_id.unwrap_or(0));
|
||||
println!(" Public: {}", calendar3.is_public);
|
||||
println!(
|
||||
" Color: {}",
|
||||
calendar3.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(" Events: {:?}", calendar3.events);
|
||||
|
||||
// --- Store Calendars in DB ---
|
||||
let cal_collection = db.collection::<Calendar>().expect("can open calendar collection");
|
||||
let cal_collection = db
|
||||
.collection::<Calendar>()
|
||||
.expect("can open calendar collection");
|
||||
|
||||
let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1");
|
||||
let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2");
|
||||
let (_, calendar3) = cal_collection.set(&calendar3).expect("can set calendar3");
|
||||
|
||||
println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name);
|
||||
println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name);
|
||||
println!(
|
||||
"Created calendar1 (ID: {}): Name - '{}' (Owner: {}, Public: {})",
|
||||
calendar1.get_id(),
|
||||
calendar1.name,
|
||||
calendar1.owner_id.unwrap_or(0),
|
||||
calendar1.is_public
|
||||
);
|
||||
println!(
|
||||
"Created calendar2 (ID: {}): Name - '{}' (Owner: {}, Public: {})",
|
||||
calendar2.get_id(),
|
||||
calendar2.name,
|
||||
calendar2.owner_id.unwrap_or(0),
|
||||
calendar2.is_public
|
||||
);
|
||||
println!(
|
||||
"Created calendar3 (ID: {}): Name - '{}' (Owner: {}, Public: {})",
|
||||
calendar3.get_id(),
|
||||
calendar3.name,
|
||||
calendar3.owner_id.unwrap_or(0),
|
||||
calendar3.is_public
|
||||
);
|
||||
|
||||
// --- Retrieve a Calendar by ID ---
|
||||
let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1");
|
||||
assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB");
|
||||
let stored_calendar1_opt = cal_collection
|
||||
.get_by_id(calendar1.get_id())
|
||||
.expect("can try to load calendar1");
|
||||
assert!(
|
||||
stored_calendar1_opt.is_some(),
|
||||
"Calendar1 should be found in DB"
|
||||
);
|
||||
let mut stored_calendar1 = stored_calendar1_opt.unwrap();
|
||||
|
||||
println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len());
|
||||
println!(
|
||||
"\nRetrieved calendar1 from DB: Name - '{}', Events count: {}",
|
||||
stored_calendar1.name,
|
||||
stored_calendar1.events.len()
|
||||
);
|
||||
assert_eq!(stored_calendar1.name, "Work Calendar");
|
||||
assert_eq!(stored_calendar1.events.len(), 2);
|
||||
assert_eq!(stored_calendar1.events[0].title, "Team Meeting");
|
||||
assert_eq!(stored_calendar1.events[0], event1_id as i64); // Check event ID
|
||||
|
||||
// --- Modify a Calendar (Reschedule an Event) ---
|
||||
let event_id_to_reschedule = event1.id.as_str();
|
||||
let new_start_time = now + Duration::seconds(10800); // 3 hours from now
|
||||
let new_end_time = now + Duration::seconds(14400); // 4 hours from now
|
||||
// --- Modify a Calendar (Add/Remove Events) ---
|
||||
// Since events are just IDs, we can add and remove them easily
|
||||
println!("\n--- Modifying Calendar ---");
|
||||
|
||||
stored_calendar1 = stored_calendar1.update_event(event_id_to_reschedule, |event_to_update| {
|
||||
println!("Rescheduling event '{}'...", event_to_update.title);
|
||||
event_to_update.reschedule(new_start_time, new_end_time)
|
||||
});
|
||||
// Create and store a new event
|
||||
let new_event = Event::new(
|
||||
"1-on-1 Meeting",
|
||||
now + Duration::days(3),
|
||||
now + Duration::days(3) + Duration::minutes(30),
|
||||
)
|
||||
.description("One-on-one meeting with team member.")
|
||||
.location("Office");
|
||||
|
||||
let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
|
||||
.expect("Rescheduled event should exist");
|
||||
assert_eq!(rescheduled_event.start_time, new_start_time);
|
||||
assert_eq!(rescheduled_event.end_time, new_end_time);
|
||||
println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title);
|
||||
let (new_event_id, _stored_new_event) =
|
||||
event_collection.set(&new_event).expect("can set new event");
|
||||
println!("Created new event with ID: {}", new_event_id);
|
||||
|
||||
// Add the new event ID to the calendar
|
||||
stored_calendar1 = stored_calendar1.add_event(new_event_id as i64);
|
||||
assert_eq!(stored_calendar1.events.len(), 3);
|
||||
println!(
|
||||
"Added event ID {} to calendar1. Total events: {}",
|
||||
new_event_id,
|
||||
stored_calendar1.events.len()
|
||||
);
|
||||
|
||||
// Remove an event ID from the calendar
|
||||
stored_calendar1 = stored_calendar1.remove_event(event2_id as i64); // Remove "Project Brainstorm"
|
||||
assert_eq!(stored_calendar1.events.len(), 2);
|
||||
println!(
|
||||
"Removed event ID {} from calendar1. Total events: {}",
|
||||
event2_id,
|
||||
stored_calendar1.events.len()
|
||||
);
|
||||
|
||||
// --- Store the modified calendar ---
|
||||
let (_, mut stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set modified calendar1");
|
||||
let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1");
|
||||
let (_, _stored_calendar1) = cal_collection
|
||||
.set(&stored_calendar1)
|
||||
.expect("can set modified calendar1");
|
||||
|
||||
// Verify the changes were persisted
|
||||
let re_retrieved_calendar1_opt = cal_collection
|
||||
.get_by_id(calendar1.get_id())
|
||||
.expect("can try to load modified calendar1");
|
||||
let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap();
|
||||
let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
|
||||
.expect("Rescheduled event should exist in re-retrieved calendar");
|
||||
assert_eq!(re_retrieved_event.start_time, new_start_time, "Reschedule not persisted correctly");
|
||||
assert_eq!(re_retrieved_calendar1.events.len(), 2);
|
||||
assert!(re_retrieved_calendar1.events.contains(&(event1_id as i64))); // Team Meeting still there
|
||||
assert!(
|
||||
re_retrieved_calendar1
|
||||
.events
|
||||
.contains(&(new_event_id as i64))
|
||||
); // New event added
|
||||
assert!(!re_retrieved_calendar1.events.contains(&(event2_id as i64))); // Project Brainstorm removed
|
||||
|
||||
println!("\nModified and re-saved calendar1. Rescheduled event start time: {}", re_retrieved_event.start_time);
|
||||
|
||||
// --- Add a new event to an existing calendar ---
|
||||
let event4_new = Event::new(
|
||||
"event_delta".to_string(),
|
||||
"1-on-1",
|
||||
now + Duration::days(3),
|
||||
now + Duration::days(3) + Duration::seconds(1800) // 30 minutes
|
||||
println!(
|
||||
"\nModified and re-saved calendar1. Final events: {:?}",
|
||||
re_retrieved_calendar1.events
|
||||
);
|
||||
stored_calendar1 = stored_calendar1.add_event(event4_new);
|
||||
assert_eq!(stored_calendar1.events.len(), 3);
|
||||
let (_, stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event");
|
||||
println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len());
|
||||
|
||||
// --- Delete a Calendar ---
|
||||
cal_collection.delete_by_id(calendar2.get_id()).expect("can delete calendar2");
|
||||
let deleted_calendar2_opt = cal_collection.get_by_id(calendar2.get_id()).expect("can try to load deleted calendar2");
|
||||
assert!(deleted_calendar2_opt.is_none(), "Calendar2 should be deleted from DB");
|
||||
cal_collection
|
||||
.delete_by_id(calendar2.get_id())
|
||||
.expect("can delete calendar2");
|
||||
let deleted_calendar2_opt = cal_collection
|
||||
.get_by_id(calendar2.get_id())
|
||||
.expect("can try to load deleted calendar2");
|
||||
assert!(
|
||||
deleted_calendar2_opt.is_none(),
|
||||
"Calendar2 should be deleted from DB"
|
||||
);
|
||||
|
||||
println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id());
|
||||
println!("Calendar model DB Prefix: {}", Calendar::db_prefix());
|
||||
|
||||
// --- Demonstrate Event Retrieval ---
|
||||
println!("\n--- Retrieving Events from Database ---");
|
||||
|
||||
// Get all events
|
||||
let all_events = event_collection.get_all().expect("can list all events");
|
||||
println!("All events in database:");
|
||||
for event in &all_events {
|
||||
println!(
|
||||
"- Event ID: {}, Title: '{}', Start: {}, Attendees: {}",
|
||||
event.get_id(),
|
||||
event.title,
|
||||
event.start_time.format("%Y-%m-%d %H:%M"),
|
||||
event.attendees.len()
|
||||
);
|
||||
}
|
||||
println!("Total events in DB: {}", all_events.len());
|
||||
|
||||
// Retrieve specific events by ID and show attendee details
|
||||
println!("\nRetrieving specific events:");
|
||||
if let Some(retrieved_event1) = event_collection
|
||||
.get_by_id(event1_id)
|
||||
.expect("can try to get event1")
|
||||
{
|
||||
println!(
|
||||
"Retrieved Event 1: '{}' with {} attendees",
|
||||
retrieved_event1.title,
|
||||
retrieved_event1.attendees.len()
|
||||
);
|
||||
|
||||
// Look up attendee details for each attendee ID
|
||||
for &attendee_id in &retrieved_event1.attendees {
|
||||
if let Some(attendee) = attendee_collection
|
||||
.get_by_id(attendee_id)
|
||||
.expect("can try to get attendee")
|
||||
{
|
||||
// Look up user details for the attendee's contact_id
|
||||
if let Some(user) = user_collection
|
||||
.get_by_id(attendee.contact_id)
|
||||
.expect("can try to get user")
|
||||
{
|
||||
println!(
|
||||
" - Attendee ID {}: {} (User: {}, Status: {:?})",
|
||||
attendee_id, user.full_name, attendee.contact_id, attendee.status
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- List All Calendars ---
|
||||
println!("\n--- Listing All Enhanced Calendars ---");
|
||||
let all_calendars = cal_collection.get_all().expect("can list all calendars");
|
||||
for calendar in &all_calendars {
|
||||
println!(
|
||||
"- Calendar ID: {}, Name: '{}', Owner: {}, Public: {}, Color: {}",
|
||||
calendar.get_id(),
|
||||
calendar.name,
|
||||
calendar.owner_id.unwrap_or(0),
|
||||
calendar.is_public,
|
||||
calendar.color.as_ref().unwrap_or(&"Default".to_string())
|
||||
);
|
||||
println!(
|
||||
" Description: {}",
|
||||
calendar.description.as_ref().unwrap_or(&"None".to_string())
|
||||
);
|
||||
println!(" Events: {:?}", calendar.events);
|
||||
|
||||
// Show which events are in this calendar with their details
|
||||
for &event_id in &calendar.events {
|
||||
if let Some(event) = event_collection
|
||||
.get_by_id(event_id as u32)
|
||||
.expect("can try to get event")
|
||||
{
|
||||
println!(
|
||||
" * Event: '{}' (Status: {:?}, Category: {}, All-day: {})",
|
||||
event.title,
|
||||
event.status,
|
||||
event.category.as_ref().unwrap_or(&"None".to_string()),
|
||||
event.all_day
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Total calendars in DB: {}", all_calendars.len());
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!("To clean up, you can manually delete the directory: {}", db_path);
|
||||
println!(
|
||||
"To clean up, you can manually delete the directory: {}",
|
||||
db_path
|
||||
);
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ fn main() {
|
||||
.add_signer(signer2.clone())
|
||||
.add_revision(revision1.clone())
|
||||
.add_revision(revision2.clone());
|
||||
|
||||
|
||||
// The `#[model]` derive handles `created_at` and `updated_at` in `base_data`.
|
||||
// `base_data.touch()` might be called internally by setters or needs explicit call if fields are set directly.
|
||||
// For builder pattern, the final state of `base_data.updated_at` reflects the time of the last builder call if `touch()` is implicit.
|
||||
@ -87,7 +87,7 @@ fn main() {
|
||||
|
||||
println!("\n--- Contract Details After Signing ---");
|
||||
println!("{:#?}", contract);
|
||||
|
||||
|
||||
println!("\n--- Accessing Specific Fields ---");
|
||||
println!("Contract Title: {}", contract.title);
|
||||
println!("Contract Status: {:?}", contract.status);
|
||||
@ -97,7 +97,10 @@ fn main() {
|
||||
println!("Updated At (timestamp): {}", contract.base_data.modified_at); // From BaseModelData
|
||||
|
||||
if let Some(first_signer_details) = contract.signers.first() {
|
||||
println!("\nFirst Signer: {} ({})", first_signer_details.name, first_signer_details.email);
|
||||
println!(
|
||||
"\nFirst Signer: {} ({})",
|
||||
first_signer_details.name, first_signer_details.email
|
||||
);
|
||||
println!(" Status: {:?}", first_signer_details.status);
|
||||
if let Some(signed_time) = first_signer_details.signed_at {
|
||||
println!(" Signed At: {}", signed_time);
|
||||
@ -110,6 +113,112 @@ fn main() {
|
||||
println!(" Created By: {}", latest_rev.created_by);
|
||||
println!(" Revision Created At: {}", latest_rev.created_at);
|
||||
}
|
||||
|
||||
|
||||
// Demonstrate reminder functionality
|
||||
println!("\n--- Reminder Functionality Demo ---");
|
||||
let current_time = current_timestamp_secs();
|
||||
|
||||
// Check if we can send reminders to signers
|
||||
for (i, signer) in contract.signers.iter().enumerate() {
|
||||
println!("\nSigner {}: {} ({})", i + 1, signer.name, signer.email);
|
||||
println!(" Status: {:?}", signer.status);
|
||||
|
||||
if signer.last_reminder_mail_sent_at.is_none() {
|
||||
println!(" Last reminder: Never sent");
|
||||
} else {
|
||||
println!(
|
||||
" Last reminder: {}",
|
||||
signer.last_reminder_mail_sent_at.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let can_send = signer.can_send_reminder(current_time);
|
||||
println!(" Can send reminder now: {}", can_send);
|
||||
|
||||
if let Some(remaining) = signer.reminder_cooldown_remaining(current_time) {
|
||||
println!(" Cooldown remaining: {} seconds", remaining);
|
||||
} else {
|
||||
println!(" No cooldown active");
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate sending a reminder to the first signer
|
||||
if let Some(first_signer) = contract.signers.get_mut(0) {
|
||||
if first_signer.can_send_reminder(current_time) {
|
||||
println!("\nSimulating reminder sent to: {}", first_signer.name);
|
||||
first_signer.mark_reminder_sent(current_time);
|
||||
println!(
|
||||
" Reminder timestamp updated to: {}",
|
||||
first_signer.last_reminder_mail_sent_at.unwrap()
|
||||
);
|
||||
|
||||
// Check cooldown after sending
|
||||
if let Some(remaining) = first_signer.reminder_cooldown_remaining(current_time) {
|
||||
println!(" New cooldown: {} seconds (30 minutes)", remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate signature functionality
|
||||
println!("\n--- Signature Functionality Demo ---");
|
||||
|
||||
// Simulate signing with signature data
|
||||
if let Some(signer_to_sign) = contract.signers.get_mut(1) {
|
||||
println!("\nBefore signing:");
|
||||
println!(
|
||||
" Signer: {} ({})",
|
||||
signer_to_sign.name, signer_to_sign.email
|
||||
);
|
||||
println!(" Status: {:?}", signer_to_sign.status);
|
||||
println!(" Signed at: {:?}", signer_to_sign.signed_at);
|
||||
println!(" Signature data: {:?}", signer_to_sign.signature_data);
|
||||
|
||||
// Example base64 signature data (1x1 transparent PNG)
|
||||
let signature_data = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string();
|
||||
|
||||
// Sign the contract with signature data
|
||||
signer_to_sign.sign(
|
||||
Some(signature_data.clone()),
|
||||
Some("I agree to all terms and conditions.".to_string()),
|
||||
);
|
||||
|
||||
println!("\nAfter signing:");
|
||||
println!(" Status: {:?}", signer_to_sign.status);
|
||||
println!(" Signed at: {:?}", signer_to_sign.signed_at);
|
||||
println!(" Comments: {:?}", signer_to_sign.comments);
|
||||
println!(
|
||||
" Signature data length: {} characters",
|
||||
signer_to_sign
|
||||
.signature_data
|
||||
.as_ref()
|
||||
.map_or(0, |s| s.len())
|
||||
);
|
||||
println!(
|
||||
" Signature data preview: {}...",
|
||||
signer_to_sign
|
||||
.signature_data
|
||||
.as_ref()
|
||||
.map_or("None".to_string(), |s| s
|
||||
.chars()
|
||||
.take(50)
|
||||
.collect::<String>())
|
||||
);
|
||||
}
|
||||
|
||||
// Demonstrate signing without signature data
|
||||
if let Some(first_signer) = contract.signers.get_mut(0) {
|
||||
println!("\nSigning without signature data:");
|
||||
println!(" Signer: {}", first_signer.name);
|
||||
|
||||
first_signer.sign(
|
||||
None,
|
||||
Some("Signed electronically without visual signature.".to_string()),
|
||||
);
|
||||
|
||||
println!(" Status after signing: {:?}", first_signer.status);
|
||||
println!(" Signature data: {:?}", first_signer.signature_data);
|
||||
println!(" Comments: {:?}", first_signer.comments);
|
||||
}
|
||||
|
||||
println!("\nLegal Contract Model demonstration complete.");
|
||||
}
|
||||
|
@ -39,14 +39,20 @@ let signer1 = new_contract_signer(signer1_id, "Alice Wonderland", "alice@example
|
||||
print(`Signer 1 ID: ${signer1.id}, Name: ${signer1.name}, Email: ${signer1.email}`);
|
||||
print(`Signer 1 Status: ${signer1.status}, Comments: ${format_optional_string(signer1.comments, "N/A")}`);
|
||||
print(`Signer 1 Signed At: ${format_optional_int(signer1.signed_at, "Not signed")}`);
|
||||
print(`Signer 1 Last Reminder: ${format_optional_int(signer1.last_reminder_mail_sent_at, "Never sent")}`);
|
||||
print(`Signer 1 Signature Data: ${format_optional_string(signer1.signature_data, "No signature")}`);
|
||||
|
||||
let signer2_id = "signer-uuid-002";
|
||||
let signer2 = new_contract_signer(signer2_id, "Bob The Builder", "bob@example.com")
|
||||
.status(SignerStatusConstants::Signed)
|
||||
.signed_at(1678886400) // Example timestamp
|
||||
.comments("Bob has already signed.");
|
||||
.comments("Bob has already signed.")
|
||||
.last_reminder_mail_sent_at(1678880000) // Example reminder timestamp
|
||||
.signature_data("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="); // Example signature
|
||||
|
||||
print(`Signer 2 ID: ${signer2.id}, Name: ${signer2.name}, Status: ${signer2.status}, Signed At: ${format_optional_int(signer2.signed_at, "N/A")}`);
|
||||
print(`Signer 2 Last Reminder: ${format_optional_int(signer2.last_reminder_mail_sent_at, "Never sent")}`);
|
||||
print(`Signer 2 Signature Data Length: ${signer2.signature_data.len()} characters`);
|
||||
|
||||
// --- Test ContractRevision Model ---
|
||||
print("\n--- Testing ContractRevision Model ---");
|
||||
@ -116,4 +122,53 @@ print("Updated Contract saved.");
|
||||
let final_retrieved_contract = get_contract_by_id(contract1_base_id);
|
||||
print(`Final Retrieved Contract - Status: ${final_retrieved_contract.status}, Description: '${final_retrieved_contract.description}'`);
|
||||
|
||||
// --- Test Reminder Functionality ---
|
||||
print("\n--- Testing Reminder Functionality ---");
|
||||
let current_time = 1678900000; // Example current timestamp
|
||||
|
||||
// Test reminder functionality on signers
|
||||
if final_retrieved_contract.signers.len() > 0 {
|
||||
let test_signer = final_retrieved_contract.signers[0];
|
||||
print(`Testing reminder for signer: ${test_signer.name}`);
|
||||
|
||||
let can_send = can_send_reminder(test_signer, current_time);
|
||||
print(`Can send reminder: ${can_send}`);
|
||||
|
||||
let cooldown_remaining = reminder_cooldown_remaining(test_signer, current_time);
|
||||
print(`Cooldown remaining: ${format_optional_int(cooldown_remaining, "No cooldown")}`);
|
||||
|
||||
// Simulate sending a reminder
|
||||
if can_send {
|
||||
print("Simulating reminder sent...");
|
||||
mark_reminder_sent(test_signer, current_time);
|
||||
print("Reminder timestamp updated");
|
||||
|
||||
// Check cooldown after sending
|
||||
let new_cooldown = reminder_cooldown_remaining(test_signer, current_time);
|
||||
print(`New cooldown: ${format_optional_int(new_cooldown, "No cooldown")} seconds`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Test Signature Functionality ---
|
||||
print("\n--- Testing Signature Functionality ---");
|
||||
|
||||
// Test signing with signature data
|
||||
let test_signer = new_contract_signer("test-signer-001", "Test Signer", "test@example.com");
|
||||
print(`Before signing: Status = ${test_signer.status}, Signature Data = ${format_optional_string(test_signer.signature_data, "None")}`);
|
||||
|
||||
// Sign with signature data
|
||||
sign(test_signer, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "I agree to the terms");
|
||||
print(`After signing: Status = ${test_signer.status}, Signature Data Length = ${test_signer.signature_data.len()}`);
|
||||
print(`Comments: ${format_optional_string(test_signer.comments, "No comments")}`);
|
||||
|
||||
// Test signing without signature data
|
||||
let test_signer2 = new_contract_signer("test-signer-002", "Test Signer 2", "test2@example.com");
|
||||
sign_without_signature(test_signer2, "Electronic signature without visual data");
|
||||
print(`Signer 2 after signing: Status = ${test_signer2.status}, Signature Data = ${format_optional_string(test_signer2.signature_data, "None")}`);
|
||||
|
||||
// Test simple signing
|
||||
let test_signer3 = new_contract_signer("test-signer-003", "Test Signer 3", "test3@example.com");
|
||||
sign_simple(test_signer3);
|
||||
print(`Signer 3 after simple signing: Status = ${test_signer3.status}`);
|
||||
|
||||
print("\nLegal Rhai example script finished.");
|
||||
|
214
heromodels/examples/payment_flow_example.rs
Normal file
214
heromodels/examples/payment_flow_example.rs
Normal file
@ -0,0 +1,214 @@
|
||||
// Payment Flow Example
|
||||
// This example demonstrates the complete payment flow for company registration,
|
||||
// including company creation with pending payment status, payment processing,
|
||||
// and status transitions.
|
||||
|
||||
use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment};
|
||||
|
||||
fn main() {
|
||||
println!("=== Payment Flow Example ===");
|
||||
println!("Demonstrating company registration with payment integration\n");
|
||||
|
||||
// Step 1: Create a company with pending payment status
|
||||
println!("Step 1: Creating company with pending payment status");
|
||||
let company = Company::new(
|
||||
"TechStart Inc.".to_string(),
|
||||
"REG-TS-2024-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@techstart.com".to_string())
|
||||
.phone("+1-555-0123".to_string())
|
||||
.website("https://techstart.com".to_string())
|
||||
.address("123 Startup Ave, Innovation City, IC 12345".to_string())
|
||||
.business_type(BusinessType::Starter)
|
||||
.industry("Technology".to_string())
|
||||
.description("A promising tech startup focused on AI solutions".to_string())
|
||||
// Note: status defaults to PendingPayment, so we don't need to set it explicitly
|
||||
.fiscal_year_end("12-31".to_string());
|
||||
|
||||
println!(" Company: {}", company.name);
|
||||
println!(" Status: {:?}", company.status);
|
||||
println!(" Registration: {}", company.registration_number);
|
||||
println!(" Company created successfully!\n");
|
||||
|
||||
// Step 2: Create a payment record for the company
|
||||
println!("Step 2: Creating payment record");
|
||||
let payment_intent_id = format!("pi_test_{}", chrono::Utc::now().timestamp());
|
||||
let payment = Payment::new(
|
||||
payment_intent_id.clone(),
|
||||
1, // Mock company ID for this example
|
||||
"yearly".to_string(),
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0, // Total amount (setup + 12 months)
|
||||
);
|
||||
|
||||
println!(" Payment Intent ID: {}", payment.payment_intent_id);
|
||||
println!(" Company ID: {}", payment.company_id);
|
||||
println!(" Payment Plan: {}", payment.payment_plan);
|
||||
println!(" Setup Fee: ${:.2}", payment.setup_fee);
|
||||
println!(" Monthly Fee: ${:.2}", payment.monthly_fee);
|
||||
println!(" Total Amount: ${:.2}", payment.total_amount);
|
||||
println!(" Status: {:?}", payment.status);
|
||||
println!(" Payment record created successfully!\n");
|
||||
|
||||
// Step 3: Simulate payment processing
|
||||
println!("Step 3: Processing payment...");
|
||||
|
||||
// Simulate some processing time
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// Complete the payment with Stripe customer ID
|
||||
let stripe_customer_id = Some(format!("cus_test_{}", chrono::Utc::now().timestamp()));
|
||||
let completed_payment = payment.complete_payment(stripe_customer_id.clone());
|
||||
|
||||
println!(" Payment completed successfully!");
|
||||
println!(" New Status: {:?}", completed_payment.status);
|
||||
println!(
|
||||
" Stripe Customer ID: {:?}",
|
||||
completed_payment.stripe_customer_id
|
||||
);
|
||||
|
||||
// Step 4: Update company status to Active
|
||||
println!("\nStep 4: Updating company status to Active");
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
|
||||
println!(" Company: {}", active_company.name);
|
||||
println!(" New Status: {:?}", active_company.status);
|
||||
println!(" Company status updated successfully!\n");
|
||||
|
||||
// Step 5: Demonstrate payment status checks
|
||||
println!("Step 5: Payment status verification");
|
||||
println!(
|
||||
" Is payment completed? {}",
|
||||
completed_payment.is_completed()
|
||||
);
|
||||
println!(" Is payment pending? {}", completed_payment.is_pending());
|
||||
println!(" Has payment failed? {}", completed_payment.has_failed());
|
||||
println!(" Is payment refunded? {}", completed_payment.is_refunded());
|
||||
|
||||
// Step 6: Demonstrate failed payment scenario
|
||||
println!("\nStep 6: Demonstrating failed payment scenario");
|
||||
|
||||
// Create another company
|
||||
let failed_company = Company::new(
|
||||
"FailCorp Ltd.".to_string(),
|
||||
"REG-FC-2024-002".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@failcorp.com".to_string())
|
||||
.business_type(BusinessType::Single)
|
||||
.industry("Consulting".to_string());
|
||||
|
||||
// Create payment for failed scenario
|
||||
let failed_payment_intent = format!("pi_fail_{}", chrono::Utc::now().timestamp());
|
||||
let failed_payment = Payment::new(
|
||||
failed_payment_intent,
|
||||
2, // Mock company ID
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
49.0,
|
||||
299.0,
|
||||
);
|
||||
|
||||
// Simulate payment failure
|
||||
let failed_payment = failed_payment.fail_payment();
|
||||
|
||||
println!(" Failed Company: {}", failed_company.name);
|
||||
println!(
|
||||
" Company Status: {:?} (remains pending)",
|
||||
failed_company.status
|
||||
);
|
||||
println!(" Payment Status: {:?}", failed_payment.status);
|
||||
println!(" Payment failed: {}", failed_payment.has_failed());
|
||||
|
||||
println!("\n=== Payment Flow Example Complete ===");
|
||||
println!("Summary:");
|
||||
println!("- Created companies with PendingPayment status by default");
|
||||
println!("- Processed successful payment and updated company to Active");
|
||||
println!("- Demonstrated failed payment scenario");
|
||||
println!("- All operations completed successfully without database persistence");
|
||||
println!("- For database examples, see the Rhai examples or unit tests");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_payment_flow() {
|
||||
// Test the basic payment flow without database persistence
|
||||
let company = Company::new(
|
||||
"Test Company".to_string(),
|
||||
"TEST-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
);
|
||||
|
||||
// Verify default status is PendingPayment
|
||||
assert_eq!(company.status, CompanyStatus::PendingPayment);
|
||||
|
||||
// Create payment
|
||||
let payment = Payment::new(
|
||||
"pi_test_123".to_string(),
|
||||
1, // Mock company ID
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
// Verify default payment status is Pending
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert!(payment.is_pending());
|
||||
assert!(!payment.is_completed());
|
||||
|
||||
// Complete payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_test_123".to_string()));
|
||||
assert_eq!(completed_payment.status, PaymentStatus::Completed);
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(!completed_payment.is_pending());
|
||||
|
||||
// Update company status
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
assert_eq!(active_company.status, CompanyStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_failure() {
|
||||
let payment = Payment::new(
|
||||
"pi_fail_123".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.0,
|
||||
1688.0,
|
||||
);
|
||||
|
||||
let failed_payment = payment.fail_payment();
|
||||
assert_eq!(failed_payment.status, PaymentStatus::Failed);
|
||||
assert!(failed_payment.has_failed());
|
||||
assert!(!failed_payment.is_completed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_refund() {
|
||||
let payment = Payment::new(
|
||||
"pi_refund_123".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
49.0,
|
||||
299.0,
|
||||
);
|
||||
|
||||
// First complete the payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_123".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
|
||||
// Then refund it
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
assert_eq!(refunded_payment.status, PaymentStatus::Refunded);
|
||||
assert!(refunded_payment.is_refunded());
|
||||
assert!(!refunded_payment.is_completed());
|
||||
}
|
||||
}
|
108
heromodels/examples/test_reminder_functionality.rs
Normal file
108
heromodels/examples/test_reminder_functionality.rs
Normal file
@ -0,0 +1,108 @@
|
||||
use heromodels::models::legal::{ContractSigner, SignerStatus};
|
||||
|
||||
// Helper function to get current timestamp
|
||||
fn current_timestamp_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("Testing ContractSigner Reminder Functionality");
|
||||
println!("==============================================\n");
|
||||
|
||||
// Test 1: Create a new signer (should have no reminder timestamp)
|
||||
println!("Test 1: New signer creation");
|
||||
let mut signer = ContractSigner::new(
|
||||
"test-signer-001".to_string(),
|
||||
"Test User".to_string(),
|
||||
"test@example.com".to_string(),
|
||||
);
|
||||
|
||||
println!(" Signer created: {}", signer.name);
|
||||
println!(" Last reminder: {:?}", signer.last_reminder_mail_sent_at);
|
||||
assert_eq!(signer.last_reminder_mail_sent_at, None);
|
||||
println!(" ✓ New signer has no reminder timestamp\n");
|
||||
|
||||
// Test 2: Check if reminder can be sent (should be true for new signer)
|
||||
println!("Test 2: Can send reminder check");
|
||||
let current_time = current_timestamp_secs();
|
||||
let can_send = signer.can_send_reminder(current_time);
|
||||
println!(" Can send reminder: {}", can_send);
|
||||
assert!(can_send);
|
||||
println!(" ✓ New signer can receive reminders\n");
|
||||
|
||||
// Test 3: Check cooldown remaining (should be None for new signer)
|
||||
println!("Test 3: Cooldown remaining check");
|
||||
let cooldown = signer.reminder_cooldown_remaining(current_time);
|
||||
println!(" Cooldown remaining: {:?}", cooldown);
|
||||
assert_eq!(cooldown, None);
|
||||
println!(" ✓ New signer has no cooldown\n");
|
||||
|
||||
// Test 4: Mark reminder as sent
|
||||
println!("Test 4: Mark reminder as sent");
|
||||
signer.mark_reminder_sent(current_time);
|
||||
println!(" Reminder marked as sent at: {}", current_time);
|
||||
println!(" Last reminder timestamp: {:?}", signer.last_reminder_mail_sent_at);
|
||||
assert_eq!(signer.last_reminder_mail_sent_at, Some(current_time));
|
||||
println!(" ✓ Reminder timestamp updated correctly\n");
|
||||
|
||||
// Test 5: Check if reminder can be sent immediately after (should be false)
|
||||
println!("Test 5: Immediate retry check");
|
||||
let can_send_again = signer.can_send_reminder(current_time);
|
||||
println!(" Can send reminder immediately: {}", can_send_again);
|
||||
assert!(!can_send_again);
|
||||
println!(" ✓ Cannot send reminder immediately after sending\n");
|
||||
|
||||
// Test 6: Check cooldown remaining (should be 30 minutes)
|
||||
println!("Test 6: Cooldown after sending");
|
||||
let cooldown_after = signer.reminder_cooldown_remaining(current_time);
|
||||
println!(" Cooldown remaining: {:?} seconds", cooldown_after);
|
||||
assert_eq!(cooldown_after, Some(30 * 60)); // 30 minutes = 1800 seconds
|
||||
println!(" ✓ Cooldown is exactly 30 minutes\n");
|
||||
|
||||
// Test 7: Test after cooldown period
|
||||
println!("Test 7: After cooldown period");
|
||||
let future_time = current_time + (31 * 60); // 31 minutes later
|
||||
let can_send_later = signer.can_send_reminder(future_time);
|
||||
let cooldown_later = signer.reminder_cooldown_remaining(future_time);
|
||||
println!(" Time: {} (31 minutes later)", future_time);
|
||||
println!(" Can send reminder: {}", can_send_later);
|
||||
println!(" Cooldown remaining: {:?}", cooldown_later);
|
||||
assert!(can_send_later);
|
||||
assert_eq!(cooldown_later, None);
|
||||
println!(" ✓ Can send reminder after cooldown period\n");
|
||||
|
||||
// Test 8: Test builder pattern with reminder timestamp
|
||||
println!("Test 8: Builder pattern with reminder timestamp");
|
||||
let signer_with_reminder = ContractSigner::new(
|
||||
"test-signer-002".to_string(),
|
||||
"Another User".to_string(),
|
||||
"another@example.com".to_string(),
|
||||
)
|
||||
.status(SignerStatus::Pending)
|
||||
.last_reminder_mail_sent_at(current_time - (20 * 60)) // 20 minutes ago
|
||||
.comments("Test signer with reminder");
|
||||
|
||||
println!(" Signer: {}", signer_with_reminder.name);
|
||||
println!(" Last reminder: {:?}", signer_with_reminder.last_reminder_mail_sent_at);
|
||||
println!(" Can send reminder: {}", signer_with_reminder.can_send_reminder(current_time));
|
||||
|
||||
let remaining = signer_with_reminder.reminder_cooldown_remaining(current_time);
|
||||
println!(" Cooldown remaining: {:?} seconds", remaining);
|
||||
assert_eq!(remaining, Some(10 * 60)); // 10 minutes remaining
|
||||
println!(" ✓ Builder pattern works correctly\n");
|
||||
|
||||
// Test 9: Test clear reminder timestamp
|
||||
println!("Test 9: Clear reminder timestamp");
|
||||
let cleared_signer = signer_with_reminder.clear_last_reminder_mail_sent_at();
|
||||
println!(" Last reminder after clear: {:?}", cleared_signer.last_reminder_mail_sent_at);
|
||||
println!(" Can send reminder: {}", cleared_signer.can_send_reminder(current_time));
|
||||
assert_eq!(cleared_signer.last_reminder_mail_sent_at, None);
|
||||
assert!(cleared_signer.can_send_reminder(current_time));
|
||||
println!(" ✓ Clear reminder timestamp works correctly\n");
|
||||
|
||||
println!("All tests passed! ✅");
|
||||
println!("ContractSigner reminder functionality is working correctly.");
|
||||
}
|
163
heromodels/examples/test_signature_functionality.rs
Normal file
163
heromodels/examples/test_signature_functionality.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use heromodels::models::legal::{ContractSigner, SignerStatus};
|
||||
|
||||
fn main() {
|
||||
println!("Testing ContractSigner Signature Functionality");
|
||||
println!("==============================================\n");
|
||||
|
||||
// Test 1: Create a new signer (should have no signature data)
|
||||
println!("Test 1: New signer creation");
|
||||
let mut signer = ContractSigner::new(
|
||||
"test-signer-001".to_string(),
|
||||
"Test User".to_string(),
|
||||
"test@example.com".to_string(),
|
||||
);
|
||||
|
||||
println!(" Signer created: {}", signer.name);
|
||||
println!(" Status: {:?}", signer.status);
|
||||
println!(" Signature data: {:?}", signer.signature_data);
|
||||
assert_eq!(signer.signature_data, None);
|
||||
assert_eq!(signer.status, SignerStatus::Pending);
|
||||
println!(" ✓ New signer has no signature data and is pending\n");
|
||||
|
||||
// Test 2: Sign with signature data
|
||||
println!("Test 2: Sign with signature data");
|
||||
let signature_data = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string();
|
||||
let comments = "I agree to all terms and conditions.".to_string();
|
||||
|
||||
signer.sign(Some(signature_data.clone()), Some(comments.clone()));
|
||||
|
||||
println!(" Status after signing: {:?}", signer.status);
|
||||
println!(" Signed at: {:?}", signer.signed_at);
|
||||
println!(" Comments: {:?}", signer.comments);
|
||||
println!(" Signature data length: {}", signer.signature_data.as_ref().unwrap().len());
|
||||
|
||||
assert_eq!(signer.status, SignerStatus::Signed);
|
||||
assert!(signer.signed_at.is_some());
|
||||
assert_eq!(signer.signature_data, Some(signature_data));
|
||||
assert_eq!(signer.comments, Some(comments));
|
||||
println!(" ✓ Signing with signature data works correctly\n");
|
||||
|
||||
// Test 3: Sign without signature data
|
||||
println!("Test 3: Sign without signature data");
|
||||
let mut signer2 = ContractSigner::new(
|
||||
"test-signer-002".to_string(),
|
||||
"Test User 2".to_string(),
|
||||
"test2@example.com".to_string(),
|
||||
);
|
||||
|
||||
signer2.sign(None, Some("Electronic signature without visual data".to_string()));
|
||||
|
||||
println!(" Status: {:?}", signer2.status);
|
||||
println!(" Signature data: {:?}", signer2.signature_data);
|
||||
println!(" Comments: {:?}", signer2.comments);
|
||||
|
||||
assert_eq!(signer2.status, SignerStatus::Signed);
|
||||
assert_eq!(signer2.signature_data, None);
|
||||
assert!(signer2.comments.is_some());
|
||||
println!(" ✓ Signing without signature data works correctly\n");
|
||||
|
||||
// Test 4: Sign with no comments or signature
|
||||
println!("Test 4: Simple signing (no signature, no comments)");
|
||||
let mut signer3 = ContractSigner::new(
|
||||
"test-signer-003".to_string(),
|
||||
"Test User 3".to_string(),
|
||||
"test3@example.com".to_string(),
|
||||
);
|
||||
|
||||
signer3.sign(None, None);
|
||||
|
||||
println!(" Status: {:?}", signer3.status);
|
||||
println!(" Signature data: {:?}", signer3.signature_data);
|
||||
println!(" Comments: {:?}", signer3.comments);
|
||||
|
||||
assert_eq!(signer3.status, SignerStatus::Signed);
|
||||
assert_eq!(signer3.signature_data, None);
|
||||
assert_eq!(signer3.comments, None);
|
||||
println!(" ✓ Simple signing works correctly\n");
|
||||
|
||||
// Test 5: Builder pattern with signature data
|
||||
println!("Test 5: Builder pattern with signature data");
|
||||
let signer_with_signature = ContractSigner::new(
|
||||
"test-signer-004".to_string(),
|
||||
"Builder User".to_string(),
|
||||
"builder@example.com".to_string(),
|
||||
)
|
||||
.status(SignerStatus::Pending)
|
||||
.signature_data("data:image/png;base64,example")
|
||||
.comments("Pre-signed with builder pattern");
|
||||
|
||||
println!(" Signer: {}", signer_with_signature.name);
|
||||
println!(" Status: {:?}", signer_with_signature.status);
|
||||
println!(" Signature data: {:?}", signer_with_signature.signature_data);
|
||||
println!(" Comments: {:?}", signer_with_signature.comments);
|
||||
|
||||
assert_eq!(signer_with_signature.signature_data, Some("data:image/png;base64,example".to_string()));
|
||||
println!(" ✓ Builder pattern with signature data works correctly\n");
|
||||
|
||||
// Test 6: Clear signature data
|
||||
println!("Test 6: Clear signature data");
|
||||
let cleared_signer = signer_with_signature.clear_signature_data();
|
||||
println!(" Signature data after clear: {:?}", cleared_signer.signature_data);
|
||||
assert_eq!(cleared_signer.signature_data, None);
|
||||
println!(" ✓ Clear signature data works correctly\n");
|
||||
|
||||
// Test 7: Serialization/Deserialization test
|
||||
println!("Test 7: Serialization/Deserialization");
|
||||
let original_signer = ContractSigner::new(
|
||||
"serialize-test".to_string(),
|
||||
"Serialize User".to_string(),
|
||||
"serialize@example.com".to_string(),
|
||||
)
|
||||
.signature_data("test-signature-data")
|
||||
.comments("Test serialization");
|
||||
|
||||
// Serialize to JSON
|
||||
let json = serde_json::to_string(&original_signer).expect("Failed to serialize");
|
||||
println!(" Serialized JSON length: {} characters", json.len());
|
||||
|
||||
// Deserialize from JSON
|
||||
let deserialized_signer: ContractSigner = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
println!(" Original signature data: {:?}", original_signer.signature_data);
|
||||
println!(" Deserialized signature data: {:?}", deserialized_signer.signature_data);
|
||||
|
||||
assert_eq!(original_signer.signature_data, deserialized_signer.signature_data);
|
||||
assert_eq!(original_signer.name, deserialized_signer.name);
|
||||
assert_eq!(original_signer.email, deserialized_signer.email);
|
||||
println!(" ✓ Serialization/Deserialization works correctly\n");
|
||||
|
||||
// Test 8: Backward compatibility test
|
||||
println!("Test 8: Backward compatibility");
|
||||
// Simulate old JSON without signature_data field
|
||||
let old_json = r#"{
|
||||
"id": "old-signer",
|
||||
"name": "Old User",
|
||||
"email": "old@example.com",
|
||||
"status": "Pending",
|
||||
"signed_at": null,
|
||||
"comments": null,
|
||||
"last_reminder_mail_sent_at": null
|
||||
}"#;
|
||||
|
||||
let old_signer: ContractSigner = serde_json::from_str(old_json).expect("Failed to deserialize old format");
|
||||
println!(" Old signer name: {}", old_signer.name);
|
||||
println!(" Old signer signature data: {:?}", old_signer.signature_data);
|
||||
|
||||
assert_eq!(old_signer.signature_data, None);
|
||||
println!(" ✓ Backward compatibility works correctly\n");
|
||||
|
||||
println!("All tests passed! ✅");
|
||||
println!("ContractSigner signature functionality is working correctly.");
|
||||
|
||||
// Summary
|
||||
println!("\n📋 Summary of Features Tested:");
|
||||
println!(" ✅ New signer creation (signature_data: None)");
|
||||
println!(" ✅ Signing with signature data");
|
||||
println!(" ✅ Signing without signature data");
|
||||
println!(" ✅ Simple signing (no data, no comments)");
|
||||
println!(" ✅ Builder pattern with signature data");
|
||||
println!(" ✅ Clear signature data functionality");
|
||||
println!(" ✅ JSON serialization/deserialization");
|
||||
println!(" ✅ Backward compatibility with old data");
|
||||
println!("\n🎯 Ready for production use!");
|
||||
}
|
318
heromodels/payment_usage.md
Normal file
318
heromodels/payment_usage.md
Normal file
@ -0,0 +1,318 @@
|
||||
# Payment Model Usage Guide
|
||||
|
||||
This document provides comprehensive instructions for AI assistants on how to use the Payment model in the heromodels repository.
|
||||
|
||||
## Overview
|
||||
|
||||
The Payment model represents a payment transaction in the system, typically associated with company registration or subscription payments. It integrates with Stripe for payment processing and maintains comprehensive status tracking.
|
||||
|
||||
## Model Structure
|
||||
|
||||
```rust
|
||||
pub struct Payment {
|
||||
pub base_data: BaseModelData, // Auto-managed ID, timestamps, comments
|
||||
pub payment_intent_id: String, // Stripe payment intent ID
|
||||
pub company_id: u32, // Foreign key to Company
|
||||
pub payment_plan: String, // "monthly", "yearly", "two_year"
|
||||
pub setup_fee: f64, // One-time setup fee
|
||||
pub monthly_fee: f64, // Recurring monthly fee
|
||||
pub total_amount: f64, // Total amount paid
|
||||
pub currency: String, // Currency code (defaults to "usd")
|
||||
pub status: PaymentStatus, // Current payment status
|
||||
pub stripe_customer_id: Option<String>, // Stripe customer ID (set on completion)
|
||||
pub created_at: i64, // Payment creation timestamp
|
||||
pub completed_at: Option<i64>, // Payment completion timestamp
|
||||
}
|
||||
|
||||
pub enum PaymentStatus {
|
||||
Pending, // Initial state - payment created but not processed
|
||||
Processing, // Payment is being processed by Stripe
|
||||
Completed, // Payment successfully completed
|
||||
Failed, // Payment processing failed
|
||||
Refunded, // Payment was refunded
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Creating a New Payment
|
||||
|
||||
```rust
|
||||
use heromodels::models::biz::{Payment, PaymentStatus};
|
||||
|
||||
// Create a new payment with required fields
|
||||
let payment = Payment::new(
|
||||
"pi_1234567890".to_string(), // Stripe payment intent ID
|
||||
company_id, // Company ID from database
|
||||
"monthly".to_string(), // Payment plan
|
||||
100.0, // Setup fee
|
||||
49.99, // Monthly fee
|
||||
149.99, // Total amount
|
||||
);
|
||||
|
||||
// Payment defaults:
|
||||
// - status: PaymentStatus::Pending
|
||||
// - currency: "usd"
|
||||
// - stripe_customer_id: None
|
||||
// - created_at: current timestamp
|
||||
// - completed_at: None
|
||||
```
|
||||
|
||||
### 2. Using Builder Pattern
|
||||
|
||||
```rust
|
||||
let payment = Payment::new(
|
||||
"pi_1234567890".to_string(),
|
||||
company_id,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.99,
|
||||
1699.88,
|
||||
)
|
||||
.currency("eur".to_string())
|
||||
.stripe_customer_id(Some("cus_existing_customer".to_string()));
|
||||
```
|
||||
|
||||
### 3. Database Operations
|
||||
|
||||
```rust
|
||||
use heromodels::db::Collection;
|
||||
|
||||
// Save payment to database
|
||||
let db = get_db()?;
|
||||
let (payment_id, saved_payment) = db.set(&payment)?;
|
||||
|
||||
// Retrieve payment by ID
|
||||
let retrieved_payment: Payment = db.get_by_id(payment_id)?.unwrap();
|
||||
|
||||
// Update payment
|
||||
let updated_payment = saved_payment.complete_payment(Some("cus_new_customer".to_string()));
|
||||
let (_, final_payment) = db.set(&updated_payment)?;
|
||||
```
|
||||
|
||||
## Payment Status Management
|
||||
|
||||
### Status Transitions
|
||||
|
||||
```rust
|
||||
// 1. Start with Pending status (default)
|
||||
let payment = Payment::new(/* ... */);
|
||||
assert!(payment.is_pending());
|
||||
|
||||
// 2. Mark as processing when Stripe starts processing
|
||||
let processing_payment = payment.process_payment();
|
||||
assert!(processing_payment.is_processing());
|
||||
|
||||
// 3. Complete payment when Stripe confirms success
|
||||
let completed_payment = processing_payment.complete_payment(Some("cus_123".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(completed_payment.completed_at.is_some());
|
||||
|
||||
// 4. Handle failure if payment fails
|
||||
let failed_payment = processing_payment.fail_payment();
|
||||
assert!(failed_payment.has_failed());
|
||||
|
||||
// 5. Refund if needed
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
assert!(refunded_payment.is_refunded());
|
||||
```
|
||||
|
||||
### Status Check Methods
|
||||
|
||||
```rust
|
||||
// Check current status
|
||||
if payment.is_pending() {
|
||||
// Show "Payment Pending" UI
|
||||
} else if payment.is_processing() {
|
||||
// Show "Processing Payment" UI
|
||||
} else if payment.is_completed() {
|
||||
// Show "Payment Successful" UI
|
||||
// Enable company features
|
||||
} else if payment.has_failed() {
|
||||
// Show "Payment Failed" UI
|
||||
// Offer retry option
|
||||
} else if payment.is_refunded() {
|
||||
// Show "Payment Refunded" UI
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Company Model
|
||||
|
||||
### Complete Payment Flow
|
||||
|
||||
```rust
|
||||
use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus};
|
||||
|
||||
// 1. Create company with pending payment status
|
||||
let company = Company::new(
|
||||
"TechStart Inc.".to_string(),
|
||||
"REG-TS-2024-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@techstart.com".to_string())
|
||||
.status(CompanyStatus::PendingPayment);
|
||||
|
||||
let (company_id, company) = db.set(&company)?;
|
||||
|
||||
// 2. Create payment for the company
|
||||
let payment = Payment::new(
|
||||
stripe_payment_intent_id,
|
||||
company_id,
|
||||
"yearly".to_string(),
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0, // Total (setup + 12 months)
|
||||
);
|
||||
|
||||
let (payment_id, payment) = db.set(&payment)?;
|
||||
|
||||
// 3. Process payment through Stripe
|
||||
let processing_payment = payment.process_payment();
|
||||
let (_, processing_payment) = db.set(&processing_payment)?;
|
||||
|
||||
// 4. On successful Stripe webhook
|
||||
let completed_payment = processing_payment.complete_payment(Some(stripe_customer_id));
|
||||
let (_, completed_payment) = db.set(&completed_payment)?;
|
||||
|
||||
// 5. Activate company
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
let (_, active_company) = db.set(&active_company)?;
|
||||
```
|
||||
|
||||
## Database Indexing
|
||||
|
||||
The Payment model provides custom indexes for efficient querying:
|
||||
|
||||
```rust
|
||||
// Indexed fields for fast lookups:
|
||||
// - payment_intent_id: Find payment by Stripe intent ID
|
||||
// - company_id: Find all payments for a company
|
||||
// - status: Find payments by status
|
||||
|
||||
// Example queries (conceptual - actual implementation depends on your query layer)
|
||||
// let pending_payments = db.find_by_index("status", "Pending")?;
|
||||
// let company_payments = db.find_by_index("company_id", company_id.to_string())?;
|
||||
// let stripe_payment = db.find_by_index("payment_intent_id", "pi_1234567890")?;
|
||||
```
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
```rust
|
||||
use heromodels::db::DbError;
|
||||
|
||||
fn process_payment_flow(payment_intent_id: String, company_id: u32) -> Result<Payment, DbError> {
|
||||
let db = get_db()?;
|
||||
|
||||
// Create payment
|
||||
let payment = Payment::new(
|
||||
payment_intent_id,
|
||||
company_id,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
49.99,
|
||||
149.99,
|
||||
);
|
||||
|
||||
// Save to database
|
||||
let (payment_id, payment) = db.set(&payment)?;
|
||||
|
||||
// Process through Stripe (external API call)
|
||||
match process_stripe_payment(&payment.payment_intent_id) {
|
||||
Ok(stripe_customer_id) => {
|
||||
// Success: complete payment
|
||||
let completed_payment = payment.complete_payment(Some(stripe_customer_id));
|
||||
let (_, final_payment) = db.set(&completed_payment)?;
|
||||
Ok(final_payment)
|
||||
}
|
||||
Err(_) => {
|
||||
// Failure: mark as failed
|
||||
let failed_payment = payment.fail_payment();
|
||||
let (_, final_payment) = db.set(&failed_payment)?;
|
||||
Ok(final_payment)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The Payment model includes comprehensive tests in `tests/payment.rs`. When working with payments:
|
||||
|
||||
1. **Always test status transitions**
|
||||
2. **Verify timestamp handling**
|
||||
3. **Test database persistence**
|
||||
4. **Test integration with Company model**
|
||||
5. **Test builder pattern methods**
|
||||
|
||||
```bash
|
||||
# Run payment tests
|
||||
cargo test payment
|
||||
|
||||
# Run specific test
|
||||
cargo test test_payment_completion
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Payment Retry Logic
|
||||
```rust
|
||||
fn retry_failed_payment(payment: Payment) -> Payment {
|
||||
if payment.has_failed() {
|
||||
// Reset to pending for retry
|
||||
Payment::new(
|
||||
payment.payment_intent_id,
|
||||
payment.company_id,
|
||||
payment.payment_plan,
|
||||
payment.setup_fee,
|
||||
payment.monthly_fee,
|
||||
payment.total_amount,
|
||||
)
|
||||
.currency(payment.currency)
|
||||
} else {
|
||||
payment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Payment Summary
|
||||
```rust
|
||||
fn get_payment_summary(payment: &Payment) -> String {
|
||||
format!(
|
||||
"Payment {} for company {}: {} {} ({})",
|
||||
payment.payment_intent_id,
|
||||
payment.company_id,
|
||||
payment.total_amount,
|
||||
payment.currency.to_uppercase(),
|
||||
payment.status
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Payment Validation
|
||||
```rust
|
||||
fn validate_payment(payment: &Payment) -> Result<(), String> {
|
||||
if payment.total_amount <= 0.0 {
|
||||
return Err("Total amount must be positive".to_string());
|
||||
}
|
||||
if payment.payment_intent_id.is_empty() {
|
||||
return Err("Payment intent ID is required".to_string());
|
||||
}
|
||||
if payment.company_id == 0 {
|
||||
return Err("Valid company ID is required".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Key Points for AI Assistants
|
||||
|
||||
1. **Always use auto-generated IDs** - Don't manually set IDs, let OurDB handle them
|
||||
2. **Follow status flow** - Pending → Processing → Completed/Failed → (optionally) Refunded
|
||||
3. **Update timestamps** - `completed_at` is automatically set when calling `complete_payment()`
|
||||
4. **Use builder pattern** - For optional fields and cleaner code
|
||||
5. **Test thoroughly** - Payment logic is critical, always verify with tests
|
||||
6. **Handle errors gracefully** - Payment failures should be tracked, not ignored
|
||||
7. **Integrate with Company** - Payments typically affect company status
|
||||
8. **Use proper indexing** - Leverage indexed fields for efficient queries
|
||||
|
||||
This model follows the heromodels patterns and integrates seamlessly with the existing codebase architecture.
|
@ -323,6 +323,16 @@ where
|
||||
assigned_id
|
||||
};
|
||||
|
||||
// Always create a primary key index entry for this model type
|
||||
// This ensures get_all() can find all objects of this type, even if they have no explicit indexed fields
|
||||
let primary_index_key = format!("{}::primary", M::db_prefix());
|
||||
let mut primary_ids: HashSet<u32> =
|
||||
Self::get_tst_value(&mut index_db, &primary_index_key)?
|
||||
.unwrap_or_else(HashSet::new);
|
||||
primary_ids.insert(assigned_id);
|
||||
let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&primary_index_key, raw_primary_ids)?;
|
||||
|
||||
// Now add the new indices
|
||||
for index_key in indices_to_add {
|
||||
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
|
||||
@ -420,6 +430,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Also remove from the primary key index
|
||||
let primary_index_key = format!("{}::primary", M::db_prefix());
|
||||
if let Some(mut primary_ids) =
|
||||
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &primary_index_key)?
|
||||
{
|
||||
primary_ids.remove(&id);
|
||||
if primary_ids.is_empty() {
|
||||
// This was the last object of this type, remove the primary index entirely
|
||||
index_db.delete(&primary_index_key)?;
|
||||
} else {
|
||||
// There are still other objects of this type, write back updated set
|
||||
let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&primary_index_key, raw_primary_ids)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally delete the object itself
|
||||
Ok(data_db.delete(id)?)
|
||||
}
|
||||
@ -428,36 +454,16 @@ where
|
||||
let mut index_db = self.index.lock().expect("can lock index DB");
|
||||
let mut data_db = self.data.lock().expect("can lock data DB");
|
||||
|
||||
let prefix = M::db_prefix();
|
||||
let mut all_object_ids: HashSet<u32> = HashSet::new();
|
||||
|
||||
// Use getall to find all index entries (values are serialized HashSet<u32>) for the given model prefix.
|
||||
match index_db.getall(prefix) {
|
||||
Ok(list_of_raw_ids_set_bytes) => {
|
||||
for raw_ids_set_bytes in list_of_raw_ids_set_bytes {
|
||||
// Each item in the list is a bincode-serialized HashSet<u32> of object IDs.
|
||||
match bincode::serde::decode_from_slice::<HashSet<u32>, _>(&raw_ids_set_bytes, BINCODE_CONFIG) {
|
||||
Ok((ids_set, _)) => { // Destructure the tuple (HashSet<u32>, usize)
|
||||
all_object_ids.extend(ids_set);
|
||||
}
|
||||
Err(e) => {
|
||||
// If deserialization of an ID set fails, propagate as a decode error.
|
||||
return Err(super::Error::Decode(e));
|
||||
}
|
||||
}
|
||||
// Look for the primary key index entry for this model type
|
||||
let primary_index_key = format!("{}::primary", M::db_prefix());
|
||||
let all_object_ids: HashSet<u32> =
|
||||
match Self::get_tst_value(&mut index_db, &primary_index_key)? {
|
||||
Some(ids) => ids,
|
||||
None => {
|
||||
// No primary index found, meaning no objects of this type exist
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
}
|
||||
Err(tst::Error::PrefixNotFound(_)) => {
|
||||
// No index entries found for this prefix, meaning no objects of this type exist.
|
||||
// Note: tst::getall might return Ok(vec![]) in this case instead of PrefixNotFound.
|
||||
// Depending on tst implementation, this arm might be redundant if getall returns empty vec.
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Err(e) => {
|
||||
// Other TST errors.
|
||||
return Err(super::Error::DB(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut results: Vec<M> = Vec::with_capacity(all_object_ids.len());
|
||||
for obj_id in all_object_ids {
|
||||
|
@ -1,19 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index};
|
||||
use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)]
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // For #[derive(CustomType)]
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CompanyStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
PendingPayment, // Company created but payment not completed
|
||||
Active, // Payment completed, company is active
|
||||
Suspended, // Company suspended (e.g., payment issues)
|
||||
Inactive, // Company deactivated
|
||||
}
|
||||
|
||||
impl Default for CompanyStatus {
|
||||
fn default() -> Self {
|
||||
CompanyStatus::Inactive
|
||||
CompanyStatus::PendingPayment
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +36,7 @@ impl Default for BusinessType {
|
||||
|
||||
// --- Company Struct ---
|
||||
|
||||
#[model]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType
|
||||
pub struct Company {
|
||||
pub base_data: BaseModelData,
|
||||
@ -51,55 +54,11 @@ pub struct Company {
|
||||
pub status: CompanyStatus,
|
||||
}
|
||||
|
||||
// --- Model Trait Implementation ---
|
||||
|
||||
impl Model for Company {
|
||||
fn db_prefix() -> &'static str {
|
||||
"company"
|
||||
}
|
||||
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
|
||||
// Override db_keys to provide custom indexes if needed
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
vec![
|
||||
IndexKeyBuilder::new("name").value(self.name.clone()).build(),
|
||||
IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(),
|
||||
// Add other relevant keys, e.g., by status or type if frequently queried
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// --- Index Implementations (Example) ---
|
||||
|
||||
pub struct CompanyNameIndex;
|
||||
impl Index for CompanyNameIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"name"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompanyRegistrationNumberIndex;
|
||||
impl Index for CompanyRegistrationNumberIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"registration_number"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Builder Pattern ---
|
||||
|
||||
impl Company {
|
||||
pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64
|
||||
pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self {
|
||||
// incorporation_date to i64
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
name,
|
||||
|
@ -2,23 +2,24 @@
|
||||
// Sub-modules will be declared here
|
||||
|
||||
pub mod company;
|
||||
pub mod payment;
|
||||
pub mod product;
|
||||
// pub mod sale;
|
||||
// pub mod shareholder;
|
||||
// pub mod user;
|
||||
|
||||
// Re-export main types from sub-modules
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub use company::{BusinessType, Company, CompanyStatus};
|
||||
pub use payment::{Payment, PaymentStatus};
|
||||
pub mod shareholder;
|
||||
pub use product::{Product, ProductComponent, ProductStatus, ProductType};
|
||||
pub use shareholder::{Shareholder, ShareholderType};
|
||||
pub use product::{Product, ProductType, ProductStatus, ProductComponent};
|
||||
|
||||
pub mod sale;
|
||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||
|
||||
// pub use user::{User}; // Assuming a simple User model for now
|
||||
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub mod rhai;
|
||||
#[cfg(feature = "rhai")]
|
||||
|
216
heromodels/src/models/biz/payment.rs
Normal file
216
heromodels/src/models/biz/payment.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // For #[derive(CustomType)]
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PaymentStatus {
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PaymentStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PaymentStatus::Pending => write!(f, "Pending"),
|
||||
PaymentStatus::Processing => write!(f, "Processing"),
|
||||
PaymentStatus::Completed => write!(f, "Completed"),
|
||||
PaymentStatus::Failed => write!(f, "Failed"),
|
||||
PaymentStatus::Refunded => write!(f, "Refunded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaymentStatus {
|
||||
fn default() -> Self {
|
||||
PaymentStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
// --- Payment Struct ---
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Payment {
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
// Stripe payment intent ID for tracking
|
||||
#[index]
|
||||
pub payment_intent_id: String,
|
||||
|
||||
// Reference to the company this payment is for
|
||||
#[index]
|
||||
pub company_id: u32,
|
||||
|
||||
// Payment plan details
|
||||
pub payment_plan: String, // "monthly", "yearly", "two_year"
|
||||
pub setup_fee: f64,
|
||||
pub monthly_fee: f64,
|
||||
pub total_amount: f64,
|
||||
pub currency: String, // "usd"
|
||||
|
||||
pub status: PaymentStatus,
|
||||
pub stripe_customer_id: Option<String>,
|
||||
pub created_at: i64, // Timestamp
|
||||
pub completed_at: Option<i64>, // Completion timestamp
|
||||
}
|
||||
|
||||
// Model trait implementation is automatically generated by #[model] attribute
|
||||
|
||||
// --- Builder Pattern ---
|
||||
|
||||
impl Payment {
|
||||
pub fn new(
|
||||
payment_intent_id: String,
|
||||
company_id: u32,
|
||||
payment_plan: String,
|
||||
setup_fee: f64,
|
||||
monthly_fee: f64,
|
||||
total_amount: f64,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
payment_intent_id,
|
||||
company_id,
|
||||
payment_plan,
|
||||
setup_fee,
|
||||
monthly_fee,
|
||||
total_amount,
|
||||
currency: "usd".to_string(), // Default to USD
|
||||
status: PaymentStatus::default(),
|
||||
stripe_customer_id: None,
|
||||
created_at: now,
|
||||
completed_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payment_intent_id(mut self, payment_intent_id: String) -> Self {
|
||||
self.payment_intent_id = payment_intent_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = company_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn payment_plan(mut self, payment_plan: String) -> Self {
|
||||
self.payment_plan = payment_plan;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn setup_fee(mut self, setup_fee: f64) -> Self {
|
||||
self.setup_fee = setup_fee;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn monthly_fee(mut self, monthly_fee: f64) -> Self {
|
||||
self.monthly_fee = monthly_fee;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn total_amount(mut self, total_amount: f64) -> Self {
|
||||
self.total_amount = total_amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: PaymentStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stripe_customer_id(mut self, stripe_customer_id: Option<String>) -> Self {
|
||||
self.stripe_customer_id = stripe_customer_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn currency(mut self, currency: String) -> Self {
|
||||
self.currency = currency;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_at(mut self, created_at: i64) -> Self {
|
||||
self.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn completed_at(mut self, completed_at: Option<i64>) -> Self {
|
||||
self.completed_at = completed_at;
|
||||
self
|
||||
}
|
||||
|
||||
// --- Business Logic Methods ---
|
||||
|
||||
/// Complete the payment with optional Stripe customer ID
|
||||
pub fn complete_payment(mut self, stripe_customer_id: Option<String>) -> Self {
|
||||
self.status = PaymentStatus::Completed;
|
||||
self.stripe_customer_id = stripe_customer_id;
|
||||
self.completed_at = Some(chrono::Utc::now().timestamp());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark payment as processing
|
||||
pub fn process_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Processing;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark payment as failed
|
||||
pub fn fail_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Refund the payment
|
||||
pub fn refund_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Refunded;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if payment is completed
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.status == PaymentStatus::Completed
|
||||
}
|
||||
|
||||
/// Check if payment is pending
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.status == PaymentStatus::Pending
|
||||
}
|
||||
|
||||
/// Check if payment is processing
|
||||
pub fn is_processing(&self) -> bool {
|
||||
self.status == PaymentStatus::Processing
|
||||
}
|
||||
|
||||
/// Check if payment has failed
|
||||
pub fn has_failed(&self) -> bool {
|
||||
self.status == PaymentStatus::Failed
|
||||
}
|
||||
|
||||
/// Check if payment is refunded
|
||||
pub fn is_refunded(&self) -> bool {
|
||||
self.status == PaymentStatus::Refunded
|
||||
}
|
||||
|
||||
// Setter for base_data fields if needed directly
|
||||
pub fn set_base_created_at(mut self, created_at: i64) -> Self {
|
||||
self.base_data.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, modified_at: i64) -> Self {
|
||||
self.base_data.modified_at = modified_at;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for Payment model are located in tests/payment.rs
|
File diff suppressed because it is too large
Load Diff
@ -1,32 +1,58 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents the status of an attendee for an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AttendanceStatus {
|
||||
Accepted,
|
||||
Declined,
|
||||
Tentative,
|
||||
NoResponse,
|
||||
Accepted = 0,
|
||||
Declined = 1,
|
||||
Tentative = 2,
|
||||
NoResponse = 3,
|
||||
}
|
||||
|
||||
/// Represents the status of an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum EventStatus {
|
||||
Draft = 0,
|
||||
Published = 1,
|
||||
Cancelled = 2,
|
||||
}
|
||||
|
||||
/// Represents an attendee of an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Attendee {
|
||||
/// Base model data
|
||||
pub base_data: BaseModelData,
|
||||
/// ID of the user attending
|
||||
// Assuming user_id might be queryable
|
||||
pub contact_id: u32,
|
||||
/// Attendance status of the user for the event
|
||||
pub status: AttendanceStatus,
|
||||
}
|
||||
|
||||
impl Attendee {
|
||||
/// Creates a new attendee with auto-generated ID
|
||||
pub fn new(contact_id: u32) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(), // ID will be auto-generated by OurDB
|
||||
contact_id,
|
||||
status: AttendanceStatus::NoResponse,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new attendee with optional ID (use None for auto-generated ID)
|
||||
pub fn new_with_id(id: Option<u32>, contact_id: u32) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
contact_id,
|
||||
status: AttendanceStatus::NoResponse,
|
||||
}
|
||||
@ -39,10 +65,10 @@ impl Attendee {
|
||||
}
|
||||
|
||||
/// Represents an event in a calendar
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Event {
|
||||
/// Base model data
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseModelData,
|
||||
/// Title of the event
|
||||
pub title: String,
|
||||
@ -52,23 +78,88 @@ pub struct Event {
|
||||
pub start_time: DateTime<Utc>,
|
||||
/// End time of the event
|
||||
pub end_time: DateTime<Utc>,
|
||||
/// List of attendees for the event
|
||||
pub attendees: Vec<Attendee>,
|
||||
/// List of attendee IDs for the event
|
||||
pub attendees: Vec<u32>,
|
||||
/// Optional location of the event
|
||||
pub location: Option<String>,
|
||||
/// Color for the event (hex color code)
|
||||
pub color: Option<String>,
|
||||
/// Whether this is an all-day event
|
||||
pub all_day: bool,
|
||||
/// ID of the user who created the event
|
||||
pub created_by: Option<u32>,
|
||||
/// Status of the event
|
||||
pub status: EventStatus,
|
||||
/// Whether this is a recurring event
|
||||
pub is_recurring: bool,
|
||||
/// Optional timezone for display purposes
|
||||
pub timezone: Option<String>,
|
||||
/// Optional category/tag for the event
|
||||
pub category: Option<String>,
|
||||
/// Optional reminder settings (minutes before event)
|
||||
pub reminder_minutes: Option<i32>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Creates a new event
|
||||
/// Converts the event to a JSON string
|
||||
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
|
||||
/// Creates an event from a JSON string
|
||||
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str(json)
|
||||
}
|
||||
|
||||
/// Creates a new event with auto-generated ID
|
||||
pub fn new(title: impl ToString, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
base_data: BaseModelData::new(), // ID will be auto-generated by OurDB
|
||||
title: title.to_string(),
|
||||
description: None,
|
||||
start_time,
|
||||
end_time,
|
||||
attendees: Vec::new(),
|
||||
location: None,
|
||||
color: Some("#4285F4".to_string()), // Default blue color
|
||||
all_day: false,
|
||||
created_by: None,
|
||||
status: EventStatus::Published,
|
||||
is_recurring: false,
|
||||
timezone: None,
|
||||
category: None,
|
||||
reminder_minutes: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new event with optional ID (use None for auto-generated ID)
|
||||
pub fn new_with_id(
|
||||
id: Option<u32>,
|
||||
title: impl ToString,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
title: title.to_string(),
|
||||
description: None,
|
||||
start_time,
|
||||
end_time,
|
||||
attendees: Vec::new(),
|
||||
location: None,
|
||||
color: Some("#4285F4".to_string()), // Default blue color
|
||||
all_day: false,
|
||||
created_by: None,
|
||||
status: EventStatus::Published,
|
||||
is_recurring: false,
|
||||
timezone: None,
|
||||
category: None,
|
||||
reminder_minutes: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,26 +181,66 @@ impl Event {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attendee to the event
|
||||
pub fn add_attendee(mut self, attendee: Attendee) -> Self {
|
||||
// Prevent duplicate attendees by contact_id
|
||||
if !self.attendees.iter().any(|a| a.contact_id == attendee.contact_id) {
|
||||
self.attendees.push(attendee);
|
||||
/// Sets the color for the event
|
||||
pub fn color(mut self, color: impl ToString) -> Self {
|
||||
self.color = Some(color.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether this is an all-day event
|
||||
pub fn all_day(mut self, all_day: bool) -> Self {
|
||||
self.all_day = all_day;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the creator of the event
|
||||
pub fn created_by(mut self, user_id: u32) -> Self {
|
||||
self.created_by = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the status of the event
|
||||
pub fn status(mut self, status: EventStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether this is a recurring event
|
||||
pub fn is_recurring(mut self, is_recurring: bool) -> Self {
|
||||
self.is_recurring = is_recurring;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the timezone for the event
|
||||
pub fn timezone(mut self, timezone: impl ToString) -> Self {
|
||||
self.timezone = Some(timezone.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the category for the event
|
||||
pub fn category(mut self, category: impl ToString) -> Self {
|
||||
self.category = Some(category.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets reminder minutes before the event
|
||||
pub fn reminder_minutes(mut self, minutes: i32) -> Self {
|
||||
self.reminder_minutes = Some(minutes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attendee ID to the event
|
||||
pub fn add_attendee(mut self, attendee_id: u32) -> Self {
|
||||
// Prevent duplicate attendees by ID
|
||||
if !self.attendees.iter().any(|&a_id| a_id == attendee_id) {
|
||||
self.attendees.push(attendee_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes an attendee from the event by user_id
|
||||
pub fn remove_attendee(mut self, contact_id: u32) -> Self {
|
||||
self.attendees.retain(|a| a.contact_id != contact_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Updates the status of an existing attendee
|
||||
pub fn update_attendee_status(mut self, contact_id: u32, status: AttendanceStatus) -> Self {
|
||||
if let Some(attendee) = self.attendees.iter_mut().find(|a| a.contact_id == contact_id) {
|
||||
attendee.status = status;
|
||||
}
|
||||
/// Removes an attendee from the event by attendee ID
|
||||
pub fn remove_attendee(mut self, attendee_id: u32) -> Self {
|
||||
self.attendees.retain(|&a_id| a_id != attendee_id);
|
||||
self
|
||||
}
|
||||
|
||||
@ -130,25 +261,24 @@ impl Event {
|
||||
}
|
||||
|
||||
/// Represents a calendar with events
|
||||
#[rhai_model_export(
|
||||
db_type = "std::sync::Arc<crate::db::hero::OurDB>",
|
||||
)]
|
||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Calendar {
|
||||
/// Base model data
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
/// Name of the calendar
|
||||
pub name: String,
|
||||
|
||||
/// Optional description of the calendar
|
||||
pub description: Option<String>,
|
||||
|
||||
/// List of events in the calendar
|
||||
// For now, events are embedded. If they become separate models, this would be Vec<[IDType]>.
|
||||
/// List of event IDs in the calendar
|
||||
pub events: Vec<i64>,
|
||||
/// ID of the user who owns this calendar
|
||||
pub owner_id: Option<u32>,
|
||||
/// Whether this calendar is public
|
||||
pub is_public: bool,
|
||||
/// Color theme for the calendar (hex color code)
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
@ -168,6 +298,9 @@ impl Calendar {
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
events: Vec::new(),
|
||||
owner_id: None,
|
||||
is_public: false,
|
||||
color: Some("#4285F4".to_string()), // Default blue color
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,6 +316,24 @@ impl Calendar {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the owner of the calendar
|
||||
pub fn owner_id(mut self, user_id: u32) -> Self {
|
||||
self.owner_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the calendar is public
|
||||
pub fn is_public(mut self, is_public: bool) -> Self {
|
||||
self.is_public = is_public;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the color for the calendar
|
||||
pub fn color(mut self, color: impl ToString) -> Self {
|
||||
self.color = Some(color.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an event to the calendar
|
||||
pub fn add_event(mut self, event_id: i64) -> Self {
|
||||
// Prevent duplicate events by id
|
||||
@ -194,7 +345,8 @@ impl Calendar {
|
||||
|
||||
/// Removes an event from the calendar by its ID
|
||||
pub fn remove_event(mut self, event_id_to_remove: i64) -> Self {
|
||||
self.events.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove);
|
||||
self.events
|
||||
.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
pub mod calendar;
|
||||
pub mod rhai;
|
||||
|
||||
// Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs
|
||||
pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
// Re-export Calendar, Event, Attendee, AttendanceStatus, and EventStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs
|
||||
pub use self::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus};
|
||||
pub use rhai::register_rhai_engine_functions as register_calendar_rhai_module;
|
||||
|
@ -1,17 +1,16 @@
|
||||
use rhai::{Engine, EvalAltResult, NativeCallContext, ImmutableString};
|
||||
use rhai::{Engine, EvalAltResult, ImmutableString, NativeCallContext};
|
||||
use std::sync::Arc;
|
||||
|
||||
use heromodels_core::BaseModelData;
|
||||
use super::calendar::{Attendee, Calendar, Event};
|
||||
use crate::db::hero::OurDB;
|
||||
use super::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method};
|
||||
use adapter_macros::rhai_timestamp_helpers;
|
||||
use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method};
|
||||
use heromodels_core::BaseModelData;
|
||||
|
||||
// Helper function for get_all_calendars registration
|
||||
|
||||
|
||||
fn new_calendar_rhai(name: String) -> Result<Calendar, Box<EvalAltResult>> {
|
||||
Ok(Calendar::new(None, name))
|
||||
Ok(Calendar::new(None, name))
|
||||
}
|
||||
|
||||
fn new_event_rhai(
|
||||
@ -20,71 +19,113 @@ fn new_event_rhai(
|
||||
start_time_ts: i64,
|
||||
end_time_ts: i64,
|
||||
) -> Result<Event, Box<EvalAltResult>> {
|
||||
let start_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts)
|
||||
.map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert start_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
)))?;
|
||||
|
||||
let end_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts)
|
||||
.map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert end_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
)))?;
|
||||
|
||||
Ok(Event::new(title_rhai.to_string(), start_time, end_time))
|
||||
}
|
||||
|
||||
pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
engine.register_fn("name", move |calendar: Calendar, name: String| Calendar::name(calendar, name));
|
||||
engine.register_fn("description", move |calendar: Calendar, description: String| Calendar::description(calendar, description));
|
||||
engine.register_fn("add_event", Calendar::add_event);
|
||||
// Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32.
|
||||
// This might require adjustment if events are fetched by ID from the DB via Calendar.events.
|
||||
|
||||
engine.register_fn("new_event", |context: NativeCallContext, title_rhai: ImmutableString, start_time_ts: i64, end_time_ts: i64| -> Result<Event, Box<EvalAltResult>> {
|
||||
new_event_rhai(context, title_rhai, start_time_ts, end_time_ts)
|
||||
});
|
||||
engine.register_fn("title", move |event: Event, title: String| Event::title(event, title));
|
||||
engine.register_fn("description", move |event: Event, description: String| Event::description(event, description));
|
||||
engine.register_fn("add_attendee", move |event: Event, attendee: Attendee| Event::add_attendee(event, attendee));
|
||||
engine.register_fn("remove_attendee", adapt_rhai_i64_input_method!(Event, remove_attendee, u32));
|
||||
engine.register_fn("update_attendee_status", move |context: NativeCallContext, event: Event, contact_id_i64: i64, status: AttendanceStatus| -> Result<Event, Box<EvalAltResult>> {
|
||||
let contact_id_u32: u32 = contact_id_i64.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for contact_id in Event::update_attendee_status from i64 to u32"),
|
||||
let start_time =
|
||||
rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts).map_err(|e_str| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert start_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
))
|
||||
})?;
|
||||
Ok(event.update_attendee_status(contact_id_u32, status))
|
||||
|
||||
let end_time =
|
||||
rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts).map_err(|e_str| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert end_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(Event::new(title_rhai.to_string(), start_time, end_time))
|
||||
}
|
||||
|
||||
pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
engine.register_fn("name", move |calendar: Calendar, name: String| {
|
||||
Calendar::name(calendar, name)
|
||||
});
|
||||
engine.register_fn(
|
||||
"description",
|
||||
move |calendar: Calendar, description: String| Calendar::description(calendar, description),
|
||||
);
|
||||
engine.register_fn("add_event", Calendar::add_event);
|
||||
// Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32.
|
||||
// This might require adjustment if events are fetched by ID from the DB via Calendar.events.
|
||||
|
||||
engine.register_fn(
|
||||
"new_event",
|
||||
|context: NativeCallContext,
|
||||
title_rhai: ImmutableString,
|
||||
start_time_ts: i64,
|
||||
end_time_ts: i64|
|
||||
-> Result<Event, Box<EvalAltResult>> {
|
||||
new_event_rhai(context, title_rhai, start_time_ts, end_time_ts)
|
||||
},
|
||||
);
|
||||
engine.register_fn("title", move |event: Event, title: String| {
|
||||
Event::title(event, title)
|
||||
});
|
||||
engine.register_fn("description", move |event: Event, description: String| {
|
||||
Event::description(event, description)
|
||||
});
|
||||
engine.register_fn(
|
||||
"add_attendee",
|
||||
adapt_rhai_i64_input_method!(Event, add_attendee, u32),
|
||||
);
|
||||
engine.register_fn(
|
||||
"remove_attendee",
|
||||
adapt_rhai_i64_input_method!(Event, remove_attendee, u32),
|
||||
);
|
||||
|
||||
engine.register_fn("new_attendee", adapt_rhai_i64_input_fn!(Attendee::new, u32));
|
||||
|
||||
engine.register_fn("new_calendar", |name: String| -> Result<Calendar, Box<EvalAltResult>> { new_calendar_rhai(name) });
|
||||
engine.register_fn(
|
||||
"new_calendar",
|
||||
|name: String| -> Result<Calendar, Box<EvalAltResult>> { new_calendar_rhai(name) },
|
||||
);
|
||||
|
||||
// Register a function to get the database instance
|
||||
engine.register_fn("get_db", move || db.clone());
|
||||
|
||||
|
||||
// Register getters for Calendar
|
||||
engine.register_get("id", |c: &mut Calendar| -> Result<i64, Box<EvalAltResult>> { Ok(c.base_data.id as i64) });
|
||||
engine.register_get("name", |c: &mut Calendar| -> Result<String, Box<EvalAltResult>> {
|
||||
// println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print
|
||||
Ok(c.name.clone())
|
||||
});
|
||||
engine.register_get("description", |c: &mut Calendar| -> Result<Option<String>, Box<EvalAltResult>> { Ok(c.description.clone()) });
|
||||
engine.register_get(
|
||||
"id",
|
||||
|c: &mut Calendar| -> Result<i64, Box<EvalAltResult>> { Ok(c.base_data.id as i64) },
|
||||
);
|
||||
engine.register_get(
|
||||
"name",
|
||||
|c: &mut Calendar| -> Result<String, Box<EvalAltResult>> {
|
||||
// println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print
|
||||
Ok(c.name.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"description",
|
||||
|c: &mut Calendar| -> Result<Option<String>, Box<EvalAltResult>> {
|
||||
Ok(c.description.clone())
|
||||
},
|
||||
);
|
||||
|
||||
// Register getter for Calendar.base_data
|
||||
engine.register_get("base_data", |c: &mut Calendar| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(c.base_data.clone()) });
|
||||
engine.register_get(
|
||||
"base_data",
|
||||
|c: &mut Calendar| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(c.base_data.clone()) },
|
||||
);
|
||||
|
||||
// Register getters for BaseModelData
|
||||
engine.register_get("id", |bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.id.into()) });
|
||||
engine.register_get(
|
||||
"id",
|
||||
|bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.id.into()) },
|
||||
);
|
||||
|
||||
// Database interaction functions for Calendar are expected to be provided by #[rhai_model_export(..)] on the Calendar struct.
|
||||
// Ensure that `db.rs` or similar correctly wires up the `OurDB` methods for these.
|
||||
|
||||
// Getters for Event
|
||||
engine.register_get("id", |e: &mut Event| -> Result<i64, Box<EvalAltResult>> { Ok(e.base_data.id as i64) });
|
||||
engine.register_get("title", |e: &mut Event| -> Result<String, Box<EvalAltResult>> { Ok(e.title.clone()) });
|
||||
engine.register_get("id", |e: &mut Event| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(e.base_data.id as i64)
|
||||
});
|
||||
engine.register_get(
|
||||
"title",
|
||||
|e: &mut Event| -> Result<String, Box<EvalAltResult>> { Ok(e.title.clone()) },
|
||||
);
|
||||
// Add more getters for Event fields as needed
|
||||
}
|
||||
|
@ -1,7 +1,18 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
/// Helper function to get current timestamp in seconds
|
||||
fn current_timestamp_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
@ -86,6 +97,8 @@ pub struct ContractSigner {
|
||||
pub status: SignerStatus,
|
||||
pub signed_at: Option<u64>, // Timestamp
|
||||
pub comments: Option<String>,
|
||||
pub last_reminder_mail_sent_at: Option<u64>, // Unix timestamp of last reminder sent
|
||||
pub signature_data: Option<String>, // Base64 encoded signature image data
|
||||
}
|
||||
|
||||
impl ContractSigner {
|
||||
@ -97,6 +110,8 @@ impl ContractSigner {
|
||||
status: SignerStatus::default(),
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
last_reminder_mail_sent_at: None,
|
||||
signature_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,7 +124,7 @@ impl ContractSigner {
|
||||
self.signed_at = Some(signed_at);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn clear_signed_at(mut self) -> Self {
|
||||
self.signed_at = None;
|
||||
self
|
||||
@ -124,6 +139,68 @@ impl ContractSigner {
|
||||
self.comments = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn last_reminder_mail_sent_at(mut self, timestamp: u64) -> Self {
|
||||
self.last_reminder_mail_sent_at = Some(timestamp);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_last_reminder_mail_sent_at(mut self) -> Self {
|
||||
self.last_reminder_mail_sent_at = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature_data(mut self, signature_data: impl ToString) -> Self {
|
||||
self.signature_data = Some(signature_data.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_signature_data(mut self) -> Self {
|
||||
self.signature_data = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Helper method to check if a reminder can be sent (30-minute rate limiting)
|
||||
pub fn can_send_reminder(&self, current_timestamp: u64) -> bool {
|
||||
match self.last_reminder_mail_sent_at {
|
||||
None => true, // No reminder sent yet
|
||||
Some(last_sent) => {
|
||||
let thirty_minutes_in_seconds = 30 * 60; // 30 minutes = 1800 seconds
|
||||
current_timestamp >= last_sent + thirty_minutes_in_seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to get remaining cooldown time in seconds
|
||||
pub fn reminder_cooldown_remaining(&self, current_timestamp: u64) -> Option<u64> {
|
||||
match self.last_reminder_mail_sent_at {
|
||||
None => None, // No cooldown if no reminder sent yet
|
||||
Some(last_sent) => {
|
||||
let thirty_minutes_in_seconds = 30 * 60; // 30 minutes = 1800 seconds
|
||||
let cooldown_end = last_sent + thirty_minutes_in_seconds;
|
||||
if current_timestamp < cooldown_end {
|
||||
Some(cooldown_end - current_timestamp)
|
||||
} else {
|
||||
None // Cooldown has expired
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to update the reminder timestamp to current time
|
||||
pub fn mark_reminder_sent(&mut self, current_timestamp: u64) {
|
||||
self.last_reminder_mail_sent_at = Some(current_timestamp);
|
||||
}
|
||||
|
||||
/// Signs the contract with optional signature data and comments
|
||||
pub fn sign(&mut self, signature_data: Option<String>, comments: Option<String>) {
|
||||
self.status = SignerStatus::Signed;
|
||||
self.signed_at = Some(current_timestamp_secs());
|
||||
self.signature_data = signature_data;
|
||||
if let Some(comment) = comments {
|
||||
self.comments = Some(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Contract Model ---
|
||||
@ -139,21 +216,21 @@ pub struct Contract {
|
||||
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
|
||||
|
||||
#[index]
|
||||
pub contract_type: String,
|
||||
|
||||
|
||||
#[index]
|
||||
pub status: crate::models::ContractStatus, // Use re-exported path for #[model] macro
|
||||
|
||||
|
||||
pub created_by: String,
|
||||
pub terms_and_conditions: String,
|
||||
|
||||
|
||||
pub start_date: Option<u64>,
|
||||
pub end_date: Option<u64>,
|
||||
pub renewal_period_days: Option<i32>,
|
||||
pub next_renewal_date: Option<u64>,
|
||||
|
||||
|
||||
pub signers: Vec<ContractSigner>,
|
||||
pub revisions: Vec<ContractRevision>,
|
||||
pub current_version: u32,
|
||||
@ -217,7 +294,7 @@ impl Contract {
|
||||
self.start_date = Some(start_date);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn clear_start_date(mut self) -> Self {
|
||||
self.start_date = None;
|
||||
self
|
||||
@ -257,7 +334,7 @@ impl Contract {
|
||||
self.signers.push(signer);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn signers(mut self, signers: Vec<ContractSigner>) -> Self {
|
||||
self.signers = signers;
|
||||
self
|
||||
@ -272,7 +349,7 @@ impl Contract {
|
||||
self.revisions = revisions;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn current_version(mut self, version: u32) -> Self {
|
||||
self.current_version = version;
|
||||
self
|
||||
@ -287,7 +364,7 @@ impl Contract {
|
||||
self.last_signed_date = None;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
// Example methods for state changes
|
||||
pub fn set_status(&mut self, status: crate::models::ContractStatus) {
|
||||
self.status = status;
|
||||
|
@ -1,51 +1,60 @@
|
||||
use rhai::{
|
||||
Dynamic, Engine, EvalAltResult, NativeCallContext, Position, Module, Array,
|
||||
};
|
||||
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module, NativeCallContext, Position};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::hero::OurDB; // Updated path based on compiler suggestion
|
||||
// use heromodels_core::BaseModelData; // Removed as fields are accessed via contract.base_data directly
|
||||
use crate::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
|
||||
use crate::models::legal::{
|
||||
Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus,
|
||||
};
|
||||
|
||||
use crate::db::Collection; // Import the Collection trait
|
||||
|
||||
// --- Helper Functions for ID and Timestamp Conversion ---
|
||||
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
fn i64_to_u32(
|
||||
val: i64,
|
||||
context_pos: Position,
|
||||
field_name: &str,
|
||||
object_name: &str,
|
||||
) -> Result<u32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u32",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
field_name, object_name, val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn i64_to_u64(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u64, Box<EvalAltResult>> {
|
||||
fn i64_to_u64(
|
||||
val: i64,
|
||||
context_pos: Position,
|
||||
field_name: &str,
|
||||
object_name: &str,
|
||||
) -> Result<u64, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u64",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
field_name, object_name, val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn i64_to_i32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<i32, Box<EvalAltResult>> {
|
||||
fn i64_to_i32(
|
||||
val: i64,
|
||||
context_pos: Position,
|
||||
field_name: &str,
|
||||
object_name: &str,
|
||||
) -> Result<i32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to i32",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
field_name, object_name, val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
@ -73,193 +82,646 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
engine.register_static_module("SignerStatusConstants", signer_status_module.into());
|
||||
engine.register_type_with_name::<SignerStatus>("SignerStatus"); // Expose the type itself
|
||||
|
||||
// --- ContractRevision ---
|
||||
// --- ContractRevision ---
|
||||
engine.register_type_with_name::<ContractRevision>("ContractRevision");
|
||||
engine.register_fn(
|
||||
"new_contract_revision",
|
||||
move |context: NativeCallContext, version_i64: i64, content: String, created_at_i64: i64, created_by: String| -> Result<ContractRevision, Box<EvalAltResult>> {
|
||||
let version = i64_to_u32(version_i64, context.position(), "version", "new_contract_revision")?;
|
||||
let created_at = i64_to_u64(created_at_i64, context.position(), "created_at", "new_contract_revision")?;
|
||||
Ok(ContractRevision::new(version, content, created_at, created_by))
|
||||
}
|
||||
move |context: NativeCallContext,
|
||||
version_i64: i64,
|
||||
content: String,
|
||||
created_at_i64: i64,
|
||||
created_by: String|
|
||||
-> Result<ContractRevision, Box<EvalAltResult>> {
|
||||
let version = i64_to_u32(
|
||||
version_i64,
|
||||
context.position(),
|
||||
"version",
|
||||
"new_contract_revision",
|
||||
)?;
|
||||
let created_at = i64_to_u64(
|
||||
created_at_i64,
|
||||
context.position(),
|
||||
"created_at",
|
||||
"new_contract_revision",
|
||||
)?;
|
||||
Ok(ContractRevision::new(
|
||||
version, content, created_at, created_by,
|
||||
))
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"comments",
|
||||
|mut revision: ContractRevision, comments: String| -> ContractRevision {
|
||||
revision.comments = Some(comments);
|
||||
revision
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"version",
|
||||
|revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(revision.version as i64)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"content",
|
||||
|revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(revision.content.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"created_at",
|
||||
|revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(revision.created_at as i64)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"created_by",
|
||||
|revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(revision.created_by.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"comments",
|
||||
|revision: &mut ContractRevision| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(revision
|
||||
.comments
|
||||
.clone()
|
||||
.map_or(Dynamic::UNIT, Dynamic::from))
|
||||
},
|
||||
);
|
||||
engine.register_fn("comments", |mut revision: ContractRevision, comments: String| -> ContractRevision {
|
||||
revision.comments = Some(comments);
|
||||
revision
|
||||
});
|
||||
engine.register_get("version", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.version as i64) });
|
||||
engine.register_get("content", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.content.clone()) });
|
||||
engine.register_get("created_at", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.created_at as i64) });
|
||||
engine.register_get("created_by", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.created_by.clone()) });
|
||||
engine.register_get("comments", |revision: &mut ContractRevision| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(revision.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
|
||||
});
|
||||
|
||||
// --- ContractSigner ---
|
||||
// --- ContractSigner ---
|
||||
engine.register_type_with_name::<ContractSigner>("ContractSigner");
|
||||
engine.register_fn(
|
||||
"new_contract_signer",
|
||||
|id: String, name: String, email: String| -> ContractSigner {
|
||||
ContractSigner::new(id, name, email)
|
||||
}
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"status",
|
||||
|signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"signed_at",
|
||||
|context: NativeCallContext,
|
||||
signer: ContractSigner,
|
||||
signed_at_i64: i64|
|
||||
-> Result<ContractSigner, Box<EvalAltResult>> {
|
||||
let signed_at_u64 = i64_to_u64(
|
||||
signed_at_i64,
|
||||
context.position(),
|
||||
"signed_at",
|
||||
"ContractSigner.signed_at",
|
||||
)?;
|
||||
Ok(signer.signed_at(signed_at_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_signed_at",
|
||||
|signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() },
|
||||
);
|
||||
engine.register_fn(
|
||||
"comments",
|
||||
|signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_comments",
|
||||
|signer: ContractSigner| -> ContractSigner { signer.clear_comments() },
|
||||
);
|
||||
engine.register_fn("status", |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) });
|
||||
engine.register_fn("signed_at", |context: NativeCallContext, signer: ContractSigner, signed_at_i64: i64| -> Result<ContractSigner, Box<EvalAltResult>> {
|
||||
let signed_at_u64 = i64_to_u64(signed_at_i64, context.position(), "signed_at", "ContractSigner.signed_at")?;
|
||||
Ok(signer.signed_at(signed_at_u64))
|
||||
});
|
||||
engine.register_fn("clear_signed_at", |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() });
|
||||
engine.register_fn("comments", |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) });
|
||||
engine.register_fn("clear_comments", |signer: ContractSigner| -> ContractSigner { signer.clear_comments() });
|
||||
|
||||
engine.register_get("id", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.id.clone()) });
|
||||
engine.register_get("name", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.name.clone()) });
|
||||
engine.register_get("email", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.email.clone()) });
|
||||
engine.register_get("status", |signer: &mut ContractSigner| -> Result<SignerStatus, Box<EvalAltResult>> { Ok(signer.status.clone()) });
|
||||
engine.register_get("signed_at_ts", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("comments", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
|
||||
});
|
||||
engine.register_get("signed_at", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts)))
|
||||
// Reminder functionality
|
||||
engine.register_fn(
|
||||
"last_reminder_mail_sent_at",
|
||||
|context: NativeCallContext,
|
||||
signer: ContractSigner,
|
||||
timestamp_i64: i64|
|
||||
-> Result<ContractSigner, Box<EvalAltResult>> {
|
||||
let timestamp_u64 = i64_to_u64(
|
||||
timestamp_i64,
|
||||
context.position(),
|
||||
"timestamp",
|
||||
"ContractSigner.last_reminder_mail_sent_at",
|
||||
)?;
|
||||
Ok(signer.last_reminder_mail_sent_at(timestamp_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_last_reminder_mail_sent_at",
|
||||
|signer: ContractSigner| -> ContractSigner { signer.clear_last_reminder_mail_sent_at() },
|
||||
);
|
||||
|
||||
// Signature data functionality
|
||||
engine.register_fn(
|
||||
"signature_data",
|
||||
|signer: ContractSigner, signature_data: String| -> ContractSigner {
|
||||
signer.signature_data(signature_data)
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_signature_data",
|
||||
|signer: ContractSigner| -> ContractSigner { signer.clear_signature_data() },
|
||||
);
|
||||
|
||||
// Helper methods for reminder logic
|
||||
engine.register_fn(
|
||||
"can_send_reminder",
|
||||
|context: NativeCallContext,
|
||||
signer: &mut ContractSigner,
|
||||
current_timestamp_i64: i64|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
let current_timestamp = i64_to_u64(
|
||||
current_timestamp_i64,
|
||||
context.position(),
|
||||
"current_timestamp",
|
||||
"ContractSigner.can_send_reminder",
|
||||
)?;
|
||||
Ok(signer.can_send_reminder(current_timestamp))
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"reminder_cooldown_remaining",
|
||||
|context: NativeCallContext,
|
||||
signer: &mut ContractSigner,
|
||||
current_timestamp_i64: i64|
|
||||
-> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let current_timestamp = i64_to_u64(
|
||||
current_timestamp_i64,
|
||||
context.position(),
|
||||
"current_timestamp",
|
||||
"ContractSigner.reminder_cooldown_remaining",
|
||||
)?;
|
||||
Ok(signer
|
||||
.reminder_cooldown_remaining(current_timestamp)
|
||||
.map_or(Dynamic::UNIT, |remaining| Dynamic::from(remaining as i64)))
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"mark_reminder_sent",
|
||||
|context: NativeCallContext,
|
||||
signer: &mut ContractSigner,
|
||||
current_timestamp_i64: i64|
|
||||
-> Result<(), Box<EvalAltResult>> {
|
||||
let current_timestamp = i64_to_u64(
|
||||
current_timestamp_i64,
|
||||
context.position(),
|
||||
"current_timestamp",
|
||||
"ContractSigner.mark_reminder_sent",
|
||||
)?;
|
||||
signer.mark_reminder_sent(current_timestamp);
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
// Sign methods
|
||||
engine.register_fn(
|
||||
"sign",
|
||||
|signer: &mut ContractSigner, signature_data: String, comments: String| {
|
||||
signer.sign(Some(signature_data), Some(comments));
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"sign_without_signature",
|
||||
|signer: &mut ContractSigner, comments: String| {
|
||||
signer.sign(None, Some(comments));
|
||||
},
|
||||
);
|
||||
engine.register_fn("sign_simple", |signer: &mut ContractSigner| {
|
||||
signer.sign(None, None);
|
||||
});
|
||||
|
||||
// --- Contract ---
|
||||
engine.register_get(
|
||||
"id",
|
||||
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(signer.id.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"name",
|
||||
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(signer.name.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"email",
|
||||
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(signer.email.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"status",
|
||||
|signer: &mut ContractSigner| -> Result<SignerStatus, Box<EvalAltResult>> {
|
||||
Ok(signer.status.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"signed_at_ts",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer
|
||||
.signed_at
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"comments",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"signed_at",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer
|
||||
.signed_at
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"last_reminder_mail_sent_at",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer
|
||||
.last_reminder_mail_sent_at
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"signature_data",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer
|
||||
.signature_data
|
||||
.as_ref()
|
||||
.map_or(Dynamic::UNIT, |data| Dynamic::from(data.clone())))
|
||||
},
|
||||
);
|
||||
|
||||
// --- Contract ---
|
||||
engine.register_type_with_name::<Contract>("Contract");
|
||||
engine.register_fn(
|
||||
"new_contract",
|
||||
move |context: NativeCallContext, base_id_i64: i64, contract_id: String| -> Result<Contract, Box<EvalAltResult>> {
|
||||
move |context: NativeCallContext,
|
||||
base_id_i64: i64,
|
||||
contract_id: String|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let base_id = i64_to_u32(base_id_i64, context.position(), "base_id", "new_contract")?;
|
||||
Ok(Contract::new(base_id, contract_id))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Builder methods
|
||||
engine.register_fn("title", |contract: Contract, title: String| -> Contract { contract.title(title) });
|
||||
engine.register_fn("description", |contract: Contract, description: String| -> Contract { contract.description(description) });
|
||||
engine.register_fn("contract_type", |contract: Contract, contract_type: String| -> Contract { contract.contract_type(contract_type) });
|
||||
engine.register_fn("status", |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) });
|
||||
engine.register_fn("created_by", |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) });
|
||||
engine.register_fn("terms_and_conditions", |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) });
|
||||
|
||||
engine.register_fn("start_date", |context: NativeCallContext, contract: Contract, start_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let start_date_u64 = i64_to_u64(start_date_i64, context.position(), "start_date", "Contract.start_date")?;
|
||||
Ok(contract.start_date(start_date_u64))
|
||||
engine.register_fn("title", |contract: Contract, title: String| -> Contract {
|
||||
contract.title(title)
|
||||
});
|
||||
engine.register_fn("clear_start_date", |contract: Contract| -> Contract { contract.clear_start_date() });
|
||||
engine.register_fn(
|
||||
"description",
|
||||
|contract: Contract, description: String| -> Contract { contract.description(description) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"contract_type",
|
||||
|contract: Contract, contract_type: String| -> Contract {
|
||||
contract.contract_type(contract_type)
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"status",
|
||||
|contract: Contract, status: ContractStatus| -> Contract { contract.status(status) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"created_by",
|
||||
|contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"terms_and_conditions",
|
||||
|contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) },
|
||||
);
|
||||
|
||||
engine.register_fn("end_date", |context: NativeCallContext, contract: Contract, end_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let end_date_u64 = i64_to_u64(end_date_i64, context.position(), "end_date", "Contract.end_date")?;
|
||||
Ok(contract.end_date(end_date_u64))
|
||||
});
|
||||
engine.register_fn("clear_end_date", |contract: Contract| -> Contract { contract.clear_end_date() });
|
||||
|
||||
engine.register_fn("renewal_period_days", |context: NativeCallContext, contract: Contract, days_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let days_i32 = i64_to_i32(days_i64, context.position(), "renewal_period_days", "Contract.renewal_period_days")?;
|
||||
Ok(contract.renewal_period_days(days_i32))
|
||||
});
|
||||
engine.register_fn("clear_renewal_period_days", |contract: Contract| -> Contract { contract.clear_renewal_period_days() });
|
||||
|
||||
engine.register_fn("next_renewal_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(date_i64, context.position(), "next_renewal_date", "Contract.next_renewal_date")?;
|
||||
Ok(contract.next_renewal_date(date_u64))
|
||||
});
|
||||
engine.register_fn("clear_next_renewal_date", |contract: Contract| -> Contract { contract.clear_next_renewal_date() });
|
||||
|
||||
engine.register_fn("add_signer", |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) });
|
||||
engine.register_fn("signers", |contract: Contract, signers_array: Array| -> Contract {
|
||||
let signers_vec = signers_array.into_iter().filter_map(|s| s.try_cast::<ContractSigner>()).collect();
|
||||
contract.signers(signers_vec)
|
||||
engine.register_fn(
|
||||
"start_date",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
start_date_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let start_date_u64 = i64_to_u64(
|
||||
start_date_i64,
|
||||
context.position(),
|
||||
"start_date",
|
||||
"Contract.start_date",
|
||||
)?;
|
||||
Ok(contract.start_date(start_date_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn("clear_start_date", |contract: Contract| -> Contract {
|
||||
contract.clear_start_date()
|
||||
});
|
||||
|
||||
engine.register_fn("add_revision", |contract: Contract, revision: ContractRevision| -> Contract { contract.add_revision(revision) });
|
||||
engine.register_fn("revisions", |contract: Contract, revisions_array: Array| -> Contract {
|
||||
let revisions_vec = revisions_array.into_iter().filter_map(|r| r.try_cast::<ContractRevision>()).collect();
|
||||
contract.revisions(revisions_vec)
|
||||
engine.register_fn(
|
||||
"end_date",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
end_date_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let end_date_u64 = i64_to_u64(
|
||||
end_date_i64,
|
||||
context.position(),
|
||||
"end_date",
|
||||
"Contract.end_date",
|
||||
)?;
|
||||
Ok(contract.end_date(end_date_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn("clear_end_date", |contract: Contract| -> Contract {
|
||||
contract.clear_end_date()
|
||||
});
|
||||
|
||||
engine.register_fn("current_version", |context: NativeCallContext, contract: Contract, version_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let version_u32 = i64_to_u32(version_i64, context.position(), "current_version", "Contract.current_version")?;
|
||||
Ok(contract.current_version(version_u32))
|
||||
});
|
||||
engine.register_fn(
|
||||
"renewal_period_days",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
days_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let days_i32 = i64_to_i32(
|
||||
days_i64,
|
||||
context.position(),
|
||||
"renewal_period_days",
|
||||
"Contract.renewal_period_days",
|
||||
)?;
|
||||
Ok(contract.renewal_period_days(days_i32))
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_renewal_period_days",
|
||||
|contract: Contract| -> Contract { contract.clear_renewal_period_days() },
|
||||
);
|
||||
|
||||
engine.register_fn("last_signed_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(date_i64, context.position(), "last_signed_date", "Contract.last_signed_date")?;
|
||||
Ok(contract.last_signed_date(date_u64))
|
||||
engine.register_fn(
|
||||
"next_renewal_date",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
date_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(
|
||||
date_i64,
|
||||
context.position(),
|
||||
"next_renewal_date",
|
||||
"Contract.next_renewal_date",
|
||||
)?;
|
||||
Ok(contract.next_renewal_date(date_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"clear_next_renewal_date",
|
||||
|contract: Contract| -> Contract { contract.clear_next_renewal_date() },
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"add_signer",
|
||||
|contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) },
|
||||
);
|
||||
engine.register_fn(
|
||||
"signers",
|
||||
|contract: Contract, signers_array: Array| -> Contract {
|
||||
let signers_vec = signers_array
|
||||
.into_iter()
|
||||
.filter_map(|s| s.try_cast::<ContractSigner>())
|
||||
.collect();
|
||||
contract.signers(signers_vec)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"add_revision",
|
||||
|contract: Contract, revision: ContractRevision| -> Contract {
|
||||
contract.add_revision(revision)
|
||||
},
|
||||
);
|
||||
engine.register_fn(
|
||||
"revisions",
|
||||
|contract: Contract, revisions_array: Array| -> Contract {
|
||||
let revisions_vec = revisions_array
|
||||
.into_iter()
|
||||
.filter_map(|r| r.try_cast::<ContractRevision>())
|
||||
.collect();
|
||||
contract.revisions(revisions_vec)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"current_version",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
version_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let version_u32 = i64_to_u32(
|
||||
version_i64,
|
||||
context.position(),
|
||||
"current_version",
|
||||
"Contract.current_version",
|
||||
)?;
|
||||
Ok(contract.current_version(version_u32))
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"last_signed_date",
|
||||
|context: NativeCallContext,
|
||||
contract: Contract,
|
||||
date_i64: i64|
|
||||
-> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(
|
||||
date_i64,
|
||||
context.position(),
|
||||
"last_signed_date",
|
||||
"Contract.last_signed_date",
|
||||
)?;
|
||||
Ok(contract.last_signed_date(date_u64))
|
||||
},
|
||||
);
|
||||
engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract {
|
||||
contract.clear_last_signed_date()
|
||||
});
|
||||
engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { contract.clear_last_signed_date() });
|
||||
|
||||
// Getters for Contract
|
||||
engine.register_get("id", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.id as i64) });
|
||||
engine.register_get("created_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.created_at as i64) });
|
||||
engine.register_get("updated_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.modified_at as i64) });
|
||||
engine.register_get("contract_id", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_id.clone()) });
|
||||
engine.register_get("title", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.title.clone()) });
|
||||
engine.register_get("description", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.description.clone()) });
|
||||
engine.register_get("contract_type", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_type.clone()) });
|
||||
engine.register_get("status", |contract: &mut Contract| -> Result<ContractStatus, Box<EvalAltResult>> { Ok(contract.status.clone()) });
|
||||
engine.register_get("created_by", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.created_by.clone()) });
|
||||
engine.register_get("terms_and_conditions", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.terms_and_conditions.clone()) });
|
||||
|
||||
engine.register_get("start_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.start_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("end_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.end_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("renewal_period_days", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.renewal_period_days.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64)))
|
||||
});
|
||||
engine.register_get("next_renewal_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.next_renewal_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("last_signed_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.last_signed_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get(
|
||||
"id",
|
||||
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(contract.base_data.id as i64)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"created_at_ts",
|
||||
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(contract.base_data.created_at as i64)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"updated_at_ts",
|
||||
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(contract.base_data.modified_at as i64)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"contract_id",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.contract_id.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"title",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.title.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"description",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.description.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"contract_type",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.contract_type.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"status",
|
||||
|contract: &mut Contract| -> Result<ContractStatus, Box<EvalAltResult>> {
|
||||
Ok(contract.status.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"created_by",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.created_by.clone())
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"terms_and_conditions",
|
||||
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
|
||||
Ok(contract.terms_and_conditions.clone())
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_get("current_version", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.current_version as i64) });
|
||||
engine.register_get(
|
||||
"start_date",
|
||||
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract
|
||||
.start_date
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"end_date",
|
||||
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract
|
||||
.end_date
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"renewal_period_days",
|
||||
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract
|
||||
.renewal_period_days
|
||||
.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"next_renewal_date",
|
||||
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract
|
||||
.next_renewal_date
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"last_signed_date",
|
||||
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract
|
||||
.last_signed_date
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_get("signers", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract.signers.iter().cloned().map(Dynamic::from).collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
engine.register_get("revisions", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract.revisions.iter().cloned().map(Dynamic::from).collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
engine.register_get(
|
||||
"current_version",
|
||||
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
|
||||
Ok(contract.current_version as i64)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_get(
|
||||
"signers",
|
||||
|contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract
|
||||
.signers
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Dynamic::from)
|
||||
.collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"revisions",
|
||||
|contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract
|
||||
.revisions
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Dynamic::from)
|
||||
.collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
},
|
||||
);
|
||||
|
||||
// Method set_status
|
||||
engine.register_fn("set_contract_status", |contract: &mut Contract, status: ContractStatus| {
|
||||
contract.set_status(status);
|
||||
});
|
||||
engine.register_fn(
|
||||
"set_contract_status",
|
||||
|contract: &mut Contract, status: ContractStatus| {
|
||||
contract.set_status(status);
|
||||
},
|
||||
);
|
||||
|
||||
// --- Database Interaction ---
|
||||
let captured_db_for_set = Arc::clone(&db);
|
||||
engine.register_fn("set_contract",
|
||||
engine.register_fn(
|
||||
"set_contract",
|
||||
move |contract: Contract| -> Result<(), Box<EvalAltResult>> {
|
||||
captured_db_for_set.set(&contract).map(|_| ()).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to set Contract (ID: {}): {}", contract.base_data.id, e).into(),
|
||||
format!(
|
||||
"Failed to set Contract (ID: {}): {}",
|
||||
contract.base_data.id, e
|
||||
)
|
||||
.into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let captured_db_for_get = Arc::clone(&db);
|
||||
engine.register_fn("get_contract_by_id",
|
||||
engine.register_fn(
|
||||
"get_contract_by_id",
|
||||
move |context: NativeCallContext, id_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_contract_by_id")?;
|
||||
|
||||
captured_db_for_get.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Error getting Contract (ID: {}): {}", id_u32, e).into(),
|
||||
Position::NONE,
|
||||
)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Contract with ID {} not found", id_u32).into(),
|
||||
Position::NONE,
|
||||
)))
|
||||
});
|
||||
|
||||
captured_db_for_get
|
||||
.get_by_id(id_u32)
|
||||
.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Error getting Contract (ID: {}): {}", id_u32, e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Contract with ID {} not found", id_u32).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ pub mod projects;
|
||||
pub use core::Comment;
|
||||
pub use userexample::User;
|
||||
// pub use productexample::Product; // Temporarily remove
|
||||
pub use biz::{Sale, SaleItem, SaleStatus};
|
||||
pub use biz::{Payment, PaymentStatus, Sale, SaleItem, SaleStatus};
|
||||
pub use calendar::{AttendanceStatus, Attendee, Calendar, Event};
|
||||
pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
|
||||
pub use finance::{Account, Asset, AssetType};
|
||||
|
313
heromodels/tests/payment.rs
Normal file
313
heromodels/tests/payment.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use heromodels::db::Collection;
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment, PaymentStatus};
|
||||
use heromodels_core::Model;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn create_test_db() -> Arc<OurDB> {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = format!("/tmp/payment_test_{}", timestamp);
|
||||
|
||||
// Clean up any existing database at this path
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
|
||||
Arc::new(OurDB::new(path, true).expect("Failed to create test database"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_creation() {
|
||||
let payment = Payment::new(
|
||||
"pi_test_123".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
assert_eq!(payment.payment_intent_id, "pi_test_123");
|
||||
assert_eq!(payment.company_id, 1);
|
||||
assert_eq!(payment.payment_plan, "monthly");
|
||||
assert_eq!(payment.setup_fee, 100.0);
|
||||
assert_eq!(payment.monthly_fee, 50.0);
|
||||
assert_eq!(payment.total_amount, 150.0);
|
||||
assert_eq!(payment.currency, "usd");
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert_eq!(payment.stripe_customer_id, None);
|
||||
assert!(payment.created_at > 0);
|
||||
assert_eq!(payment.completed_at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_default() {
|
||||
let payment = Payment::new(
|
||||
"pi_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.0,
|
||||
1688.0,
|
||||
);
|
||||
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert!(payment.is_pending());
|
||||
assert!(!payment.is_processing());
|
||||
assert!(!payment.is_completed());
|
||||
assert!(!payment.has_failed());
|
||||
assert!(!payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_processing() {
|
||||
let payment = Payment::new(
|
||||
"pi_processing_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
150.0,
|
||||
60.0,
|
||||
210.0,
|
||||
);
|
||||
|
||||
let processing_payment = payment.process_payment();
|
||||
|
||||
assert_eq!(processing_payment.status, PaymentStatus::Processing);
|
||||
assert!(processing_payment.is_processing());
|
||||
assert!(!processing_payment.is_pending());
|
||||
assert!(!processing_payment.is_completed());
|
||||
assert!(!processing_payment.has_failed());
|
||||
assert!(!processing_payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_completion() {
|
||||
let payment = Payment::new(
|
||||
"pi_complete_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
200.0,
|
||||
75.0,
|
||||
275.0,
|
||||
);
|
||||
|
||||
let stripe_customer_id = Some("cus_test_123".to_string());
|
||||
let completed_payment = payment.complete_payment(stripe_customer_id.clone());
|
||||
|
||||
assert_eq!(completed_payment.status, PaymentStatus::Completed);
|
||||
assert_eq!(completed_payment.stripe_customer_id, stripe_customer_id);
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(!completed_payment.is_pending());
|
||||
assert!(!completed_payment.has_failed());
|
||||
assert!(!completed_payment.is_refunded());
|
||||
assert!(completed_payment.completed_at.is_some());
|
||||
assert!(completed_payment.completed_at.unwrap() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_failure() {
|
||||
let payment = Payment::new(
|
||||
"pi_fail_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
300.0,
|
||||
60.0,
|
||||
1020.0,
|
||||
);
|
||||
|
||||
let failed_payment = payment.fail_payment();
|
||||
|
||||
assert_eq!(failed_payment.status, PaymentStatus::Failed);
|
||||
assert!(failed_payment.has_failed());
|
||||
assert!(!failed_payment.is_completed());
|
||||
assert!(!failed_payment.is_pending());
|
||||
assert!(!failed_payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_refund() {
|
||||
let payment = Payment::new(
|
||||
"pi_refund_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
150.0,
|
||||
45.0,
|
||||
195.0,
|
||||
);
|
||||
|
||||
// First complete the payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_refund_test".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
|
||||
// Then refund it
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
|
||||
assert_eq!(refunded_payment.status, PaymentStatus::Refunded);
|
||||
assert!(refunded_payment.is_refunded());
|
||||
assert!(!refunded_payment.is_completed());
|
||||
assert!(!refunded_payment.is_pending());
|
||||
assert!(!refunded_payment.has_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_builder_pattern() {
|
||||
let custom_timestamp = 1640995200; // Jan 1, 2022
|
||||
let payment = Payment::new(
|
||||
"pi_builder_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
)
|
||||
.payment_plan("yearly".to_string())
|
||||
.setup_fee(500.0)
|
||||
.monthly_fee(99.0)
|
||||
.total_amount(1688.0)
|
||||
.currency("eur".to_string())
|
||||
.stripe_customer_id(Some("cus_builder_test".to_string()))
|
||||
.created_at(custom_timestamp)
|
||||
.completed_at(Some(custom_timestamp + 3600));
|
||||
|
||||
assert_eq!(payment.payment_plan, "yearly");
|
||||
assert_eq!(payment.setup_fee, 500.0);
|
||||
assert_eq!(payment.monthly_fee, 99.0);
|
||||
assert_eq!(payment.total_amount, 1688.0);
|
||||
assert_eq!(payment.currency, "eur");
|
||||
assert_eq!(
|
||||
payment.stripe_customer_id,
|
||||
Some("cus_builder_test".to_string())
|
||||
);
|
||||
assert_eq!(payment.created_at, custom_timestamp);
|
||||
assert_eq!(payment.completed_at, Some(custom_timestamp + 3600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_database_persistence() {
|
||||
let db = create_test_db();
|
||||
|
||||
let payment = Payment::new(
|
||||
"pi_db_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
200.0,
|
||||
60.0,
|
||||
260.0,
|
||||
);
|
||||
|
||||
// Save payment
|
||||
let (payment_id, saved_payment) = db.set(&payment).expect("Failed to save payment");
|
||||
assert!(payment_id > 0);
|
||||
assert_eq!(saved_payment.payment_intent_id, "pi_db_test");
|
||||
|
||||
// Retrieve payment
|
||||
let retrieved_payment: Payment = db
|
||||
.get_by_id(payment_id)
|
||||
.expect("Failed to get payment")
|
||||
.unwrap();
|
||||
assert_eq!(retrieved_payment.payment_intent_id, "pi_db_test");
|
||||
assert_eq!(retrieved_payment.company_id, 1);
|
||||
assert_eq!(retrieved_payment.status, PaymentStatus::Pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_transitions() {
|
||||
let db = create_test_db();
|
||||
|
||||
let payment = Payment::new(
|
||||
"pi_transition_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
400.0,
|
||||
80.0,
|
||||
1360.0,
|
||||
);
|
||||
|
||||
let (payment_id, mut payment) = db.set(&payment).expect("Failed to save payment");
|
||||
|
||||
// Test pending -> completed
|
||||
payment = payment.complete_payment(Some("cus_transition_test".to_string()));
|
||||
let (_, mut payment) = db.set(&payment).expect("Failed to update payment");
|
||||
assert!(payment.is_completed());
|
||||
|
||||
// Test completed -> refunded
|
||||
payment = payment.refund_payment();
|
||||
let (_, payment) = db.set(&payment).expect("Failed to update payment");
|
||||
assert!(payment.is_refunded());
|
||||
|
||||
// Verify final state in database
|
||||
let final_payment: Payment = db
|
||||
.get_by_id(payment_id)
|
||||
.expect("Failed to get payment")
|
||||
.unwrap();
|
||||
assert_eq!(final_payment.status, PaymentStatus::Refunded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_timestamps() {
|
||||
let payment = Payment::new(
|
||||
"pi_timestamp_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
let initial_created_at = payment.base_data.created_at;
|
||||
let initial_modified_at = payment.base_data.modified_at;
|
||||
|
||||
// Complete payment (should update modified_at)
|
||||
let completed_payment = payment.complete_payment(Some("cus_timestamp_test".to_string()));
|
||||
|
||||
assert_eq!(completed_payment.base_data.created_at, initial_created_at);
|
||||
assert!(completed_payment.base_data.modified_at >= initial_modified_at);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_company_payment_integration() {
|
||||
let db = create_test_db();
|
||||
|
||||
// Create company with default PendingPayment status
|
||||
let company = Company::new(
|
||||
"Integration Test Corp".to_string(),
|
||||
"ITC-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("test@integration.com".to_string())
|
||||
.business_type(BusinessType::Starter);
|
||||
|
||||
let (company_id, company) = db.set(&company).expect("Failed to save company");
|
||||
assert_eq!(company.status, CompanyStatus::PendingPayment);
|
||||
|
||||
// Create payment for the company
|
||||
let payment = Payment::new(
|
||||
"pi_integration_test".to_string(),
|
||||
company_id,
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
55.0,
|
||||
305.0,
|
||||
);
|
||||
|
||||
let (_payment_id, payment) = db.set(&payment).expect("Failed to save payment");
|
||||
assert_eq!(payment.company_id, company_id);
|
||||
|
||||
// Complete payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_integration_test".to_string()));
|
||||
let (_, completed_payment) = db
|
||||
.set(&completed_payment)
|
||||
.expect("Failed to update payment");
|
||||
|
||||
// Update company status to Active
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
let (_, active_company) = db.set(&active_company).expect("Failed to update company");
|
||||
|
||||
// Verify final states
|
||||
assert!(completed_payment.is_completed());
|
||||
assert_eq!(active_company.status, CompanyStatus::Active);
|
||||
|
||||
// Verify relationship
|
||||
assert_eq!(completed_payment.company_id, active_company.get_id());
|
||||
}
|
@ -56,6 +56,8 @@ pub mut:
|
||||
status SignerStatus
|
||||
signed_at ourtime.OurTime // Optional in Rust, OurTime can be zero
|
||||
comments string // Optional in Rust, string can be empty
|
||||
last_reminder_mail_sent_at ourtime.OurTime // Unix timestamp of last reminder sent
|
||||
signature_data string // Base64 encoded signature image data (Optional in Rust)
|
||||
}
|
||||
|
||||
// SignerStatus defines the status of a contract signer
|
||||
|
Loading…
Reference in New Issue
Block a user