feat: Improve database handling of model indices #6
@@ -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(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
@@ -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
											
										
									
								
							@@ -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());
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user