diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index 67c5be5..4b317e8 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -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"] diff --git a/heromodels/examples/biz_rhai/biz.rs b/heromodels/examples/biz_rhai/biz.rs index a044707..ad86a7b 100644 --- a/heromodels/examples/biz_rhai/biz.rs +++ b/heromodels/examples/biz_rhai/biz.rs @@ -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."); diff --git a/heromodels/examples/biz_rhai/payment_flow.rhai b/heromodels/examples/biz_rhai/payment_flow.rhai new file mode 100644 index 0000000..ce777b3 --- /dev/null +++ b/heromodels/examples/biz_rhai/payment_flow.rhai @@ -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 +} diff --git a/heromodels/examples/biz_rhai/payment_flow_example.rs b/heromodels/examples/biz_rhai/payment_flow_example.rs new file mode 100644 index 0000000..61b9986 --- /dev/null +++ b/heromodels/examples/biz_rhai/payment_flow_example.rs @@ -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> { + 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> { + // 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> { + // 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 = engine.eval(constants_script)?; + assert_eq!(results, vec![true, true, true, true]); + + Ok(()) + } + + #[test] + fn test_company_status_integration() -> Result<(), Box> { + // 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 = 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 = db.get_all()?; + let company = companies + .into_iter() + .find(|c| c.name == "Integration Test") + .unwrap(); + assert_eq!(company.status, CompanyStatus::Active); + + Ok(()) + } +} diff --git a/heromodels/examples/payment_flow_example.rs b/heromodels/examples/payment_flow_example.rs new file mode 100644 index 0000000..c07eaa1 --- /dev/null +++ b/heromodels/examples/payment_flow_example.rs @@ -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()); + } +} diff --git a/heromodels/payment_usage.md b/heromodels/payment_usage.md new file mode 100644 index 0000000..666b448 --- /dev/null +++ b/heromodels/payment_usage.md @@ -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, // Stripe customer ID (set on completion) + pub created_at: i64, // Payment creation timestamp + pub completed_at: Option, // 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 { + 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. diff --git a/heromodels/src/models/biz/company.rs b/heromodels/src/models/biz/company.rs index 7fcd02d..ae55759 100644 --- a/heromodels/src/models/biz/company.rs +++ b/heromodels/src/models/biz/company.rs @@ -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 { - 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, diff --git a/heromodels/src/models/biz/mod.rs b/heromodels/src/models/biz/mod.rs index 27a1718..15ace26 100644 --- a/heromodels/src/models/biz/mod.rs +++ b/heromodels/src/models/biz/mod.rs @@ -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")] diff --git a/heromodels/src/models/biz/payment.rs b/heromodels/src/models/biz/payment.rs new file mode 100644 index 0000000..d678889 --- /dev/null +++ b/heromodels/src/models/biz/payment.rs @@ -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, + pub created_at: i64, // Timestamp + pub completed_at: Option, // 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) -> 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) -> 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) -> 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 diff --git a/heromodels/src/models/biz/rhai.rs b/heromodels/src/models/biz/rhai.rs index 7a8c2ec..327a028 100644 --- a/heromodels/src/models/biz/rhai.rs +++ b/heromodels/src/models/biz/rhai.rs @@ -1,12 +1,13 @@ -use rhai::{Engine, Module, Dynamic, EvalAltResult, Position}; -use std::sync::Arc; +use super::company::{BusinessType, Company, CompanyStatus}; +use super::payment::{Payment, PaymentStatus}; use crate::db::Collection; // For db.set and db.get_by_id use crate::db::hero::OurDB; -use super::company::{Company, CompanyStatus, BusinessType}; -use crate::models::biz::shareholder::{Shareholder, ShareholderType}; -use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent}; +use crate::models::biz::product::{Product, ProductComponent, ProductStatus, ProductType}; use crate::models::biz::sale::{Sale, SaleItem, SaleStatus}; +use crate::models::biz::shareholder::{Shareholder, ShareholderType}; use heromodels_core::Model; +use rhai::{Dynamic, Engine, EvalAltResult, Module, Position}; +use std::sync::Arc; // Helper function to convert i64 to u32, returning a Rhai error if conversion fails fn id_from_i64(id_val: i64) -> Result> { @@ -23,9 +24,13 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { // --- Enum Constants: CompanyStatus --- let mut status_constants_module = Module::new(); + status_constants_module.set_var( + "PendingPayment", + Dynamic::from(CompanyStatus::PendingPayment.clone()), + ); status_constants_module.set_var("Active", Dynamic::from(CompanyStatus::Active.clone())); - status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); status_constants_module.set_var("Suspended", Dynamic::from(CompanyStatus::Suspended.clone())); + status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); engine.register_static_module("CompanyStatusConstants", status_constants_module.into()); engine.register_type_with_name::("CompanyStatus"); @@ -36,80 +41,319 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { business_type_constants_module.set_var("Twin", Dynamic::from(BusinessType::Twin.clone())); business_type_constants_module.set_var("Starter", Dynamic::from(BusinessType::Starter.clone())); business_type_constants_module.set_var("Global", Dynamic::from(BusinessType::Global.clone())); - engine.register_static_module("BusinessTypeConstants", business_type_constants_module.into()); + engine.register_static_module( + "BusinessTypeConstants", + business_type_constants_module.into(), + ); engine.register_type_with_name::("BusinessType"); - // --- Company --- + // --- Company --- engine.register_type_with_name::("Company"); // Constructor - engine.register_fn("new_company", |name: String, registration_number: String, incorporation_date: i64| -> Result> { Ok(Company::new(name, registration_number, incorporation_date)) }); + engine.register_fn( + "new_company", + |name: String, + registration_number: String, + incorporation_date: i64| + -> Result> { + Ok(Company::new(name, registration_number, incorporation_date)) + }, + ); // Getters for Company - engine.register_get("id", |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }); - engine.register_get("created_at", |company: &mut Company| -> Result> { Ok(company.base_data.created_at) }); - engine.register_get("modified_at", |company: &mut Company| -> Result> { Ok(company.base_data.modified_at) }); - engine.register_get("name", |company: &mut Company| -> Result> { Ok(company.name.clone()) }); - engine.register_get("registration_number", |company: &mut Company| -> Result> { Ok(company.registration_number.clone()) }); - engine.register_get("incorporation_date", |company: &mut Company| -> Result> { Ok(company.incorporation_date as i64) }); - engine.register_get("fiscal_year_end", |company: &mut Company| -> Result> { Ok(company.fiscal_year_end.clone()) }); - engine.register_get("email", |company: &mut Company| -> Result> { Ok(company.email.clone()) }); - engine.register_get("phone", |company: &mut Company| -> Result> { Ok(company.phone.clone()) }); - engine.register_get("website", |company: &mut Company| -> Result> { Ok(company.website.clone()) }); - engine.register_get("address", |company: &mut Company| -> Result> { Ok(company.address.clone()) }); - engine.register_get("business_type", |company: &mut Company| -> Result> { Ok(company.business_type.clone()) }); - engine.register_get("industry", |company: &mut Company| -> Result> { Ok(company.industry.clone()) }); - engine.register_get("description", |company: &mut Company| -> Result> { Ok(company.description.clone()) }); - engine.register_get("status", |company: &mut Company| -> Result> { Ok(company.status.clone()) }); + engine.register_get( + "id", + |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }, + ); + engine.register_get( + "created_at", + |company: &mut Company| -> Result> { + Ok(company.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |company: &mut Company| -> Result> { + Ok(company.base_data.modified_at) + }, + ); + engine.register_get( + "name", + |company: &mut Company| -> Result> { Ok(company.name.clone()) }, + ); + engine.register_get( + "registration_number", + |company: &mut Company| -> Result> { + Ok(company.registration_number.clone()) + }, + ); + engine.register_get( + "incorporation_date", + |company: &mut Company| -> Result> { + Ok(company.incorporation_date as i64) + }, + ); + engine.register_get( + "fiscal_year_end", + |company: &mut Company| -> Result> { + Ok(company.fiscal_year_end.clone()) + }, + ); + engine.register_get( + "email", + |company: &mut Company| -> Result> { Ok(company.email.clone()) }, + ); + engine.register_get( + "phone", + |company: &mut Company| -> Result> { Ok(company.phone.clone()) }, + ); + engine.register_get( + "website", + |company: &mut Company| -> Result> { + Ok(company.website.clone()) + }, + ); + engine.register_get( + "address", + |company: &mut Company| -> Result> { + Ok(company.address.clone()) + }, + ); + engine.register_get( + "business_type", + |company: &mut Company| -> Result> { + Ok(company.business_type.clone()) + }, + ); + engine.register_get( + "industry", + |company: &mut Company| -> Result> { + Ok(company.industry.clone()) + }, + ); + engine.register_get( + "description", + |company: &mut Company| -> Result> { + Ok(company.description.clone()) + }, + ); + engine.register_get( + "status", + |company: &mut Company| -> Result> { + Ok(company.status.clone()) + }, + ); // Builder methods for Company - engine.register_fn("fiscal_year_end", |company: Company, fiscal_year_end: String| -> Result> { Ok(company.fiscal_year_end(fiscal_year_end)) }); - engine.register_fn("email", |company: Company, email: String| -> Result> { Ok(company.email(email)) }); - engine.register_fn("phone", |company: Company, phone: String| -> Result> { Ok(company.phone(phone)) }); - engine.register_fn("website", |company: Company, website: String| -> Result> { Ok(company.website(website)) }); - engine.register_fn("address", |company: Company, address: String| -> Result> { Ok(company.address(address)) }); - engine.register_fn("business_type", |company: Company, business_type: BusinessType| -> Result> { Ok(company.business_type(business_type)) }); - engine.register_fn("industry", |company: Company, industry: String| -> Result> { Ok(company.industry(industry)) }); - engine.register_fn("description", |company: Company, description: String| -> Result> { Ok(company.description(description)) }); - engine.register_fn("status", |company: Company, status: CompanyStatus| -> Result> { Ok(company.status(status)) }); - engine.register_fn("set_base_created_at", |company: Company, created_at: i64| -> Result> { Ok(company.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |company: Company, modified_at: i64| -> Result> { Ok(company.set_base_modified_at(modified_at)) }); + engine.register_fn( + "fiscal_year_end", + |company: Company, fiscal_year_end: String| -> Result> { + Ok(company.fiscal_year_end(fiscal_year_end)) + }, + ); + engine.register_fn( + "email", + |company: Company, email: String| -> Result> { + Ok(company.email(email)) + }, + ); + engine.register_fn( + "phone", + |company: Company, phone: String| -> Result> { + Ok(company.phone(phone)) + }, + ); + engine.register_fn( + "website", + |company: Company, website: String| -> Result> { + Ok(company.website(website)) + }, + ); + engine.register_fn( + "address", + |company: Company, address: String| -> Result> { + Ok(company.address(address)) + }, + ); + engine.register_fn( + "business_type", + |company: Company, business_type: BusinessType| -> Result> { + Ok(company.business_type(business_type)) + }, + ); + engine.register_fn( + "industry", + |company: Company, industry: String| -> Result> { + Ok(company.industry(industry)) + }, + ); + engine.register_fn( + "description", + |company: Company, description: String| -> Result> { + Ok(company.description(description)) + }, + ); + engine.register_fn( + "status", + |company: Company, status: CompanyStatus| -> Result> { + Ok(company.status(status)) + }, + ); + engine.register_fn( + "set_base_created_at", + |company: Company, created_at: i64| -> Result> { + Ok(company.set_base_created_at(created_at)) + }, + ); + engine.register_fn( + "set_base_modified_at", + |company: Company, modified_at: i64| -> Result> { + Ok(company.set_base_modified_at(modified_at)) + }, + ); // --- Enum Constants: ShareholderType --- let mut shareholder_type_constants_module = Module::new(); - shareholder_type_constants_module.set_var("Individual", Dynamic::from(ShareholderType::Individual.clone())); - shareholder_type_constants_module.set_var("Corporate", Dynamic::from(ShareholderType::Corporate.clone())); - engine.register_static_module("ShareholderTypeConstants", shareholder_type_constants_module.into()); + shareholder_type_constants_module.set_var( + "Individual", + Dynamic::from(ShareholderType::Individual.clone()), + ); + shareholder_type_constants_module.set_var( + "Corporate", + Dynamic::from(ShareholderType::Corporate.clone()), + ); + engine.register_static_module( + "ShareholderTypeConstants", + shareholder_type_constants_module.into(), + ); engine.register_type_with_name::("ShareholderType"); // --- Shareholder --- engine.register_type_with_name::("Shareholder"); // Constructor for Shareholder (minimal, takes only ID) - engine.register_fn("new_shareholder", || -> Result> { Ok(Shareholder::new()) }); + engine.register_fn( + "new_shareholder", + || -> Result> { Ok(Shareholder::new()) }, + ); // Getters for Shareholder - engine.register_get("id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.get_id() as i64) }); - engine.register_get("created_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.created_at) }); - engine.register_get("modified_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.modified_at) }); - engine.register_get("company_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.company_id as i64) }); - engine.register_get("user_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.user_id as i64) }); - engine.register_get("name", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.name.clone()) }); - engine.register_get("shares", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.shares) }); - engine.register_get("percentage", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.percentage) }); - engine.register_get("type_", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.type_.clone()) }); - engine.register_get("since", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.since) }); + engine.register_get( + "id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.get_id() as i64) + }, + ); + engine.register_get( + "created_at", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.base_data.modified_at) + }, + ); + engine.register_get( + "company_id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.company_id as i64) + }, + ); + engine.register_get( + "user_id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.user_id as i64) + }, + ); + engine.register_get( + "name", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.name.clone()) + }, + ); + engine.register_get( + "shares", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.shares) + }, + ); + engine.register_get( + "percentage", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.percentage) + }, + ); + engine.register_get( + "type_", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.type_.clone()) + }, + ); + engine.register_get( + "since", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.since) + }, + ); // Builder methods for Shareholder - engine.register_fn("company_id", |shareholder: Shareholder, company_id: i64| -> Result> { Ok(shareholder.company_id(company_id as u32)) }); - engine.register_fn("user_id", |shareholder: Shareholder, user_id: i64| -> Result> { Ok(shareholder.user_id(user_id as u32)) }); - engine.register_fn("name", |shareholder: Shareholder, name: String| -> Result> { Ok(shareholder.name(name)) }); - engine.register_fn("shares", |shareholder: Shareholder, shares: f64| -> Result> { Ok(shareholder.shares(shares)) }); - engine.register_fn("percentage", |shareholder: Shareholder, percentage: f64| -> Result> { Ok(shareholder.percentage(percentage)) }); - engine.register_fn("type_", |shareholder: Shareholder, type_: ShareholderType| -> Result> { Ok(shareholder.type_(type_)) }); - engine.register_fn("since", |shareholder: Shareholder, since: i64| -> Result> { Ok(shareholder.since(since)) }); - engine.register_fn("set_base_created_at", |shareholder: Shareholder, created_at: i64| -> Result> { Ok(shareholder.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |shareholder: Shareholder, modified_at: i64| -> Result> { Ok(shareholder.set_base_modified_at(modified_at)) }); - + engine.register_fn( + "company_id", + |shareholder: Shareholder, company_id: i64| -> Result> { + Ok(shareholder.company_id(company_id as u32)) + }, + ); + engine.register_fn( + "user_id", + |shareholder: Shareholder, user_id: i64| -> Result> { + Ok(shareholder.user_id(user_id as u32)) + }, + ); + engine.register_fn( + "name", + |shareholder: Shareholder, name: String| -> Result> { + Ok(shareholder.name(name)) + }, + ); + engine.register_fn( + "shares", + |shareholder: Shareholder, shares: f64| -> Result> { + Ok(shareholder.shares(shares)) + }, + ); + engine.register_fn( + "percentage", + |shareholder: Shareholder, percentage: f64| -> Result> { + Ok(shareholder.percentage(percentage)) + }, + ); + engine.register_fn( + "type_", + |shareholder: Shareholder, + type_: ShareholderType| + -> Result> { Ok(shareholder.type_(type_)) }, + ); + engine.register_fn( + "since", + |shareholder: Shareholder, since: i64| -> Result> { + Ok(shareholder.since(since)) + }, + ); + engine.register_fn( + "set_base_created_at", + |shareholder: Shareholder, created_at: i64| -> Result> { + Ok(shareholder.set_base_created_at(created_at)) + }, + ); + engine.register_fn( + "set_base_modified_at", + |shareholder: Shareholder, modified_at: i64| -> Result> { + Ok(shareholder.set_base_modified_at(modified_at)) + }, + ); // --- Enum Constants: ProductType --- let mut product_type_constants_module = Module::new(); @@ -120,9 +364,16 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { // --- Enum Constants: ProductStatus --- let mut product_status_constants_module = Module::new(); - product_status_constants_module.set_var("Available", Dynamic::from(ProductStatus::Available.clone())); - product_status_constants_module.set_var("Unavailable", Dynamic::from(ProductStatus::Unavailable.clone())); - engine.register_static_module("ProductStatusConstants", product_status_constants_module.into()); + product_status_constants_module + .set_var("Available", Dynamic::from(ProductStatus::Available.clone())); + product_status_constants_module.set_var( + "Unavailable", + Dynamic::from(ProductStatus::Unavailable.clone()), + ); + engine.register_static_module( + "ProductStatusConstants", + product_status_constants_module.into(), + ); engine.register_type_with_name::("ProductStatus"); // --- Enum Constants: SaleStatus --- @@ -133,200 +384,887 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { engine.register_static_module("SaleStatusConstants", sale_status_module.into()); engine.register_type_with_name::("SaleStatus"); + // --- Enum Constants: PaymentStatus --- + let mut payment_status_module = Module::new(); + payment_status_module.set_var("Pending", Dynamic::from(PaymentStatus::Pending.clone())); + payment_status_module.set_var("Completed", Dynamic::from(PaymentStatus::Completed.clone())); + payment_status_module.set_var("Failed", Dynamic::from(PaymentStatus::Failed.clone())); + payment_status_module.set_var("Refunded", Dynamic::from(PaymentStatus::Refunded.clone())); + engine.register_static_module("PaymentStatusConstants", payment_status_module.into()); + engine.register_type_with_name::("PaymentStatus"); + // --- ProductComponent --- - engine.register_type_with_name::("ProductComponent") - .register_fn("new_product_component", |name: String| -> Result> { Ok(ProductComponent::new(name)) }) - .register_get("name", |pc: &mut ProductComponent| -> Result> { Ok(pc.name.clone()) }) - .register_fn("name", |pc: ProductComponent, name: String| -> Result> { Ok(pc.name(name)) }) - .register_get("description", |pc: &mut ProductComponent| -> Result> { Ok(pc.description.clone()) }) - .register_fn("description", |pc: ProductComponent, description: String| -> Result> { Ok(pc.description(description)) }) - .register_get("quantity", |pc: &mut ProductComponent| -> Result> { Ok(pc.quantity as i64) }) - .register_fn("quantity", |pc: ProductComponent, quantity: i64| -> Result> { Ok(pc.quantity(quantity as u32)) }); + engine + .register_type_with_name::("ProductComponent") + .register_fn( + "new_product_component", + |name: String| -> Result> { + Ok(ProductComponent::new(name)) + }, + ) + .register_get( + "name", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.name.clone()) + }, + ) + .register_fn( + "name", + |pc: ProductComponent, name: String| -> Result> { + Ok(pc.name(name)) + }, + ) + .register_get( + "description", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.description.clone()) + }, + ) + .register_fn( + "description", + |pc: ProductComponent, + description: String| + -> Result> { + Ok(pc.description(description)) + }, + ) + .register_get( + "quantity", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.quantity as i64) + }, + ) + .register_fn( + "quantity", + |pc: ProductComponent, quantity: i64| -> Result> { + Ok(pc.quantity(quantity as u32)) + }, + ); // --- Product --- - engine.register_type_with_name::("Product") - .register_fn("new_product", || -> Result> { Ok(Product::new()) }) - // Getters for Product - .register_get("id", |p: &mut Product| -> Result> { Ok(p.base_data.id as i64) }) - .register_get("name", |p: &mut Product| -> Result> { Ok(p.name.clone()) }) - .register_get("description", |p: &mut Product| -> Result> { Ok(p.description.clone()) }) - .register_get("price", |p: &mut Product| -> Result> { Ok(p.price) }) - .register_get("type_", |p: &mut Product| -> Result> { Ok(p.type_.clone()) }) - .register_get("category", |p: &mut Product| -> Result> { Ok(p.category.clone()) }) - .register_get("status", |p: &mut Product| -> Result> { Ok(p.status.clone()) }) - .register_get("max_amount", |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }) - .register_get("purchase_till", |p: &mut Product| -> Result> { Ok(p.purchase_till) }) - .register_get("active_till", |p: &mut Product| -> Result> { Ok(p.active_till) }) - .register_get("components", |p: &mut Product| -> Result> { - let rhai_array = p.components.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) + engine + .register_type_with_name::("Product") + .register_fn("new_product", || -> Result> { + Ok(Product::new()) }) + // Getters for Product + .register_get("id", |p: &mut Product| -> Result> { + Ok(p.base_data.id as i64) + }) + .register_get( + "name", + |p: &mut Product| -> Result> { Ok(p.name.clone()) }, + ) + .register_get( + "description", + |p: &mut Product| -> Result> { Ok(p.description.clone()) }, + ) + .register_get( + "price", + |p: &mut Product| -> Result> { Ok(p.price) }, + ) + .register_get( + "type_", + |p: &mut Product| -> Result> { Ok(p.type_.clone()) }, + ) + .register_get( + "category", + |p: &mut Product| -> Result> { Ok(p.category.clone()) }, + ) + .register_get( + "status", + |p: &mut Product| -> Result> { Ok(p.status.clone()) }, + ) + .register_get( + "max_amount", + |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }, + ) + .register_get( + "purchase_till", + |p: &mut Product| -> Result> { Ok(p.purchase_till) }, + ) + .register_get( + "active_till", + |p: &mut Product| -> Result> { Ok(p.active_till) }, + ) + .register_get( + "components", + |p: &mut Product| -> Result> { + let rhai_array = p + .components + .iter() + .cloned() + .map(Dynamic::from) + .collect::(); + Ok(rhai_array) + }, + ) // Getters for BaseModelData fields - .register_get("created_at", |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }) - .register_get("modified_at", |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }) - .register_get("comments", |p: &mut Product| -> Result, Box> { Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) }) + .register_get( + "created_at", + |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }, + ) + .register_get( + "modified_at", + |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }, + ) + .register_get( + "comments", + |p: &mut Product| -> Result, Box> { + Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) + }, + ) // Builder methods for Product - .register_fn("name", |p: Product, name: String| -> Result> { Ok(p.name(name)) }) - .register_fn("description", |p: Product, description: String| -> Result> { Ok(p.description(description)) }) - .register_fn("price", |p: Product, price: f64| -> Result> { Ok(p.price(price)) }) - .register_fn("type_", |p: Product, type_: ProductType| -> Result> { Ok(p.type_(type_)) }) - .register_fn("category", |p: Product, category: String| -> Result> { Ok(p.category(category)) }) - .register_fn("status", |p: Product, status: ProductStatus| -> Result> { Ok(p.status(status)) }) - .register_fn("max_amount", |p: Product, max_amount: i64| -> Result> { Ok(p.max_amount(max_amount as u16)) }) - .register_fn("purchase_till", |p: Product, purchase_till: i64| -> Result> { Ok(p.purchase_till(purchase_till)) }) - .register_fn("active_till", |p: Product, active_till: i64| -> Result> { Ok(p.active_till(active_till)) }) - .register_fn("add_component", |p: Product, component: ProductComponent| -> Result> { Ok(p.add_component(component)) }) - .register_fn("components", |p: Product, components: Vec| -> Result> { Ok(p.components(components)) }) - .register_fn("set_base_created_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_modified_at(time)) }) - .register_fn("add_base_comment_id", |p: Product, comment_id: i64| -> Result> { Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) }) - .register_fn("set_base_comment_ids", |p: Product, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(p.set_base_comment_ids(u32_ids)) - }); + .register_fn( + "name", + |p: Product, name: String| -> Result> { Ok(p.name(name)) }, + ) + .register_fn( + "description", + |p: Product, description: String| -> Result> { + Ok(p.description(description)) + }, + ) + .register_fn( + "price", + |p: Product, price: f64| -> Result> { Ok(p.price(price)) }, + ) + .register_fn( + "type_", + |p: Product, type_: ProductType| -> Result> { + Ok(p.type_(type_)) + }, + ) + .register_fn( + "category", + |p: Product, category: String| -> Result> { + Ok(p.category(category)) + }, + ) + .register_fn( + "status", + |p: Product, status: ProductStatus| -> Result> { + Ok(p.status(status)) + }, + ) + .register_fn( + "max_amount", + |p: Product, max_amount: i64| -> Result> { + Ok(p.max_amount(max_amount as u16)) + }, + ) + .register_fn( + "purchase_till", + |p: Product, purchase_till: i64| -> Result> { + Ok(p.purchase_till(purchase_till)) + }, + ) + .register_fn( + "active_till", + |p: Product, active_till: i64| -> Result> { + Ok(p.active_till(active_till)) + }, + ) + .register_fn( + "add_component", + |p: Product, component: ProductComponent| -> Result> { + Ok(p.add_component(component)) + }, + ) + .register_fn( + "components", + |p: Product, + components: Vec| + -> Result> { Ok(p.components(components)) }, + ) + .register_fn( + "set_base_created_at", + |p: Product, time: i64| -> Result> { + Ok(p.set_base_created_at(time)) + }, + ) + .register_fn( + "set_base_modified_at", + |p: Product, time: i64| -> Result> { + Ok(p.set_base_modified_at(time)) + }, + ) + .register_fn( + "add_base_comment_id", + |p: Product, comment_id: i64| -> Result> { + Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) + }, + ) + .register_fn( + "set_base_comment_ids", + |p: Product, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids + .into_iter() + .map(id_from_i64) + .collect::, _>>()?; + Ok(p.set_base_comment_ids(u32_ids)) + }, + ); // --- SaleItem --- engine.register_type_with_name::("SaleItem"); - engine.register_fn("new_sale_item", |product_id_i64: i64, name: String, quantity_i64: i64, unit_price: f64, subtotal: f64| -> Result> { - Ok(SaleItem::new(id_from_i64(product_id_i64)?, name, quantity_i64 as i32, unit_price, subtotal)) - }); + engine.register_fn( + "new_sale_item", + |product_id_i64: i64, + name: String, + quantity_i64: i64, + unit_price: f64, + subtotal: f64| + -> Result> { + Ok(SaleItem::new( + id_from_i64(product_id_i64)?, + name, + quantity_i64 as i32, + unit_price, + subtotal, + )) + }, + ); // Getters for SaleItem - engine.register_get("product_id", |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }); - engine.register_get("name", |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }); - engine.register_get("quantity", |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }); - engine.register_get("unit_price", |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }); - engine.register_get("subtotal", |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }); - engine.register_get("service_active_until", |si: &mut SaleItem| -> Result, Box> { Ok(si.service_active_until) }); + engine.register_get( + "product_id", + |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }, + ); + engine.register_get( + "name", + |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }, + ); + engine.register_get( + "quantity", + |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }, + ); + engine.register_get( + "unit_price", + |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }, + ); + engine.register_get( + "subtotal", + |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }, + ); + engine.register_get( + "service_active_until", + |si: &mut SaleItem| -> Result, Box> { + Ok(si.service_active_until) + }, + ); // Builder-style methods for SaleItem - engine.register_type_with_name::("SaleItem") - .register_fn("product_id", |item: SaleItem, product_id_i64: i64| -> Result> { Ok(item.product_id(id_from_i64(product_id_i64)?)) }) - .register_fn("name", |item: SaleItem, name: String| -> Result> { Ok(item.name(name)) }) - .register_fn("quantity", |item: SaleItem, quantity_i64: i64| -> Result> { Ok(item.quantity(quantity_i64 as i32)) }) - .register_fn("unit_price", |item: SaleItem, unit_price: f64| -> Result> { Ok(item.unit_price(unit_price)) }) - .register_fn("subtotal", |item: SaleItem, subtotal: f64| -> Result> { Ok(item.subtotal(subtotal)) }) - .register_fn("service_active_until", |item: SaleItem, until: Option| -> Result> { Ok(item.service_active_until(until)) }); + engine + .register_type_with_name::("SaleItem") + .register_fn( + "product_id", + |item: SaleItem, product_id_i64: i64| -> Result> { + Ok(item.product_id(id_from_i64(product_id_i64)?)) + }, + ) + .register_fn( + "name", + |item: SaleItem, name: String| -> Result> { + Ok(item.name(name)) + }, + ) + .register_fn( + "quantity", + |item: SaleItem, quantity_i64: i64| -> Result> { + Ok(item.quantity(quantity_i64 as i32)) + }, + ) + .register_fn( + "unit_price", + |item: SaleItem, unit_price: f64| -> Result> { + Ok(item.unit_price(unit_price)) + }, + ) + .register_fn( + "subtotal", + |item: SaleItem, subtotal: f64| -> Result> { + Ok(item.subtotal(subtotal)) + }, + ) + .register_fn( + "service_active_until", + |item: SaleItem, until: Option| -> Result> { + Ok(item.service_active_until(until)) + }, + ); // --- Sale --- engine.register_type_with_name::("Sale"); - engine.register_fn("new_sale", |company_id_i64: i64, buyer_name: String, buyer_email: String, total_amount: f64, status: SaleStatus, sale_date: i64| -> Result> { Ok(Sale::new(id_from_i64(company_id_i64)?, buyer_name, buyer_email, total_amount, status, sale_date)) }); + engine.register_fn( + "new_sale", + |company_id_i64: i64, + buyer_name: String, + buyer_email: String, + total_amount: f64, + status: SaleStatus, + sale_date: i64| + -> Result> { + Ok(Sale::new( + id_from_i64(company_id_i64)?, + buyer_name, + buyer_email, + total_amount, + status, + sale_date, + )) + }, + ); // Getters for Sale - engine.register_get("id", |s: &mut Sale| -> Result> { Ok(s.get_id() as i64) }); - engine.register_get("customer_id", |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }); - engine.register_get("buyer_name", |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }); - engine.register_get("buyer_email", |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }); - engine.register_get("total_amount", |s: &mut Sale| -> Result> { Ok(s.total_amount) }); - engine.register_get("status", |s: &mut Sale| -> Result> { Ok(s.status.clone()) }); - engine.register_get("sale_date", |s: &mut Sale| -> Result> { Ok(s.sale_date) }); - engine.register_get("items", |s: &mut Sale| -> Result> { - Ok(s.items.iter().cloned().map(Dynamic::from).collect::()) + engine.register_get("id", |s: &mut Sale| -> Result> { + Ok(s.get_id() as i64) }); - engine.register_get("notes", |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }); - engine.register_get("created_at", |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }); - engine.register_get("modified_at", |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }); + engine.register_get( + "customer_id", + |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }, + ); + engine.register_get( + "buyer_name", + |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }, + ); + engine.register_get( + "buyer_email", + |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }, + ); + engine.register_get( + "total_amount", + |s: &mut Sale| -> Result> { Ok(s.total_amount) }, + ); + engine.register_get( + "status", + |s: &mut Sale| -> Result> { Ok(s.status.clone()) }, + ); + engine.register_get( + "sale_date", + |s: &mut Sale| -> Result> { Ok(s.sale_date) }, + ); + engine.register_get( + "items", + |s: &mut Sale| -> Result> { + Ok(s.items + .iter() + .cloned() + .map(Dynamic::from) + .collect::()) + }, + ); + engine.register_get( + "notes", + |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }, + ); + engine.register_get( + "created_at", + |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }, + ); + engine.register_get( + "modified_at", + |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }, + ); // engine.register_get("uuid", |s: &mut Sale| -> Result, Box> { Ok(s.base_data().uuid.clone()) }); // UUID not in BaseModelData - engine.register_get("comments", |s: &mut Sale| -> Result> { - Ok(s.base_data.comments.iter().map(|&id| Dynamic::from(id as i64)).collect::()) - }); + engine.register_get( + "comments", + |s: &mut Sale| -> Result> { + Ok(s.base_data + .comments + .iter() + .map(|&id| Dynamic::from(id as i64)) + .collect::()) + }, + ); // Builder-style methods for Sale - engine.register_type_with_name::("Sale") - .register_fn("customer_id", |s: Sale, customer_id_i64: i64| -> Result> { Ok(s.company_id(id_from_i64(customer_id_i64)?)) }) - .register_fn("buyer_name", |s: Sale, buyer_name: String| -> Result> { Ok(s.buyer_name(buyer_name)) }) - .register_fn("buyer_email", |s: Sale, buyer_email: String| -> Result> { Ok(s.buyer_email(buyer_email)) }) - .register_fn("total_amount", |s: Sale, total_amount: f64| -> Result> { Ok(s.total_amount(total_amount)) }) - .register_fn("status", |s: Sale, status: SaleStatus| -> Result> { Ok(s.status(status)) }) - .register_fn("sale_date", |s: Sale, sale_date: i64| -> Result> { Ok(s.sale_date(sale_date)) }) - .register_fn("add_item", |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }) - .register_fn("items", |s: Sale, items: Vec| -> Result> { Ok(s.items(items)) }) - .register_fn("notes", |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }) - .register_fn("set_base_id", |s: Sale, id_i64: i64| -> Result> { Ok(s.set_base_id(id_from_i64(id_i64)?)) }) + engine + .register_type_with_name::("Sale") + .register_fn( + "customer_id", + |s: Sale, customer_id_i64: i64| -> Result> { + Ok(s.company_id(id_from_i64(customer_id_i64)?)) + }, + ) + .register_fn( + "buyer_name", + |s: Sale, buyer_name: String| -> Result> { + Ok(s.buyer_name(buyer_name)) + }, + ) + .register_fn( + "buyer_email", + |s: Sale, buyer_email: String| -> Result> { + Ok(s.buyer_email(buyer_email)) + }, + ) + .register_fn( + "total_amount", + |s: Sale, total_amount: f64| -> Result> { + Ok(s.total_amount(total_amount)) + }, + ) + .register_fn( + "status", + |s: Sale, status: SaleStatus| -> Result> { + Ok(s.status(status)) + }, + ) + .register_fn( + "sale_date", + |s: Sale, sale_date: i64| -> Result> { + Ok(s.sale_date(sale_date)) + }, + ) + .register_fn( + "add_item", + |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }, + ) + .register_fn( + "items", + |s: Sale, items: Vec| -> Result> { + Ok(s.items(items)) + }, + ) + .register_fn( + "notes", + |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }, + ) + .register_fn( + "set_base_id", + |s: Sale, id_i64: i64| -> Result> { + Ok(s.set_base_id(id_from_i64(id_i64)?)) + }, + ) // .register_fn("set_base_uuid", |s: Sale, uuid: Option| -> Result> { Ok(s.set_base_uuid(uuid)) }) // UUID not in BaseModelData - .register_fn("set_base_created_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_modified_at(time)) }) - .register_fn("add_base_comment", |s: Sale, comment_id_i64: i64| -> Result> { Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) }) - .register_fn("set_base_comments", |s: Sale, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(s.set_base_comments(u32_ids)) - }); + .register_fn( + "set_base_created_at", + |s: Sale, time: i64| -> Result> { + Ok(s.set_base_created_at(time)) + }, + ) + .register_fn( + "set_base_modified_at", + |s: Sale, time: i64| -> Result> { + Ok(s.set_base_modified_at(time)) + }, + ) + .register_fn( + "add_base_comment", + |s: Sale, comment_id_i64: i64| -> Result> { + Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) + }, + ) + .register_fn( + "set_base_comments", + |s: Sale, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids + .into_iter() + .map(id_from_i64) + .collect::, _>>()?; + Ok(s.set_base_comments(u32_ids)) + }, + ); // DB functions for Product let captured_db_for_set_product = Arc::clone(&db); - engine.register_fn("set_product", move |product: Product| -> Result> { - let original_id_for_error = product.get_id(); - captured_db_for_set_product.set(&product) - .map(|(_id, updated_product)| updated_product) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Product (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_product", + move |product: Product| -> Result> { + let original_id_for_error = product.get_id(); + captured_db_for_set_product + .set(&product) + .map(|(_id, updated_product)| updated_product) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Product (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_prod = Arc::clone(&db); - engine.register_fn("get_product_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_prod.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Product (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Product with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_product_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_prod + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Product (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Product with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // DB functions for Sale let captured_db_for_set_sale = Arc::clone(&db); - engine.register_fn("set_sale", move |sale: Sale| -> Result> { - let original_id_for_error = sale.get_id(); - captured_db_for_set_sale.set(&sale) - .map(|(_id, updated_sale)| updated_sale) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Sale (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_sale", + move |sale: Sale| -> Result> { + let original_id_for_error = sale.get_id(); + captured_db_for_set_sale + .set(&sale) + .map(|(_id, updated_sale)| updated_sale) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Sale (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_sale = Arc::clone(&db); - engine.register_fn("get_sale_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_from_i64(id_i64)?; - captured_db_for_get_sale.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Sale with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_sale_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_from_i64(id_i64)?; + captured_db_for_get_sale + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Sale with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // Mock DB functions for Shareholder let captured_db_for_set_shareholder = Arc::clone(&db); - engine.register_fn("set_shareholder", move |shareholder: Shareholder| -> Result> { - let original_id_for_error = shareholder.get_id(); - captured_db_for_set_shareholder.set(&shareholder) - .map(|(_id, updated_shareholder)| updated_shareholder) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Shareholder (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_shareholder", + move |shareholder: Shareholder| -> Result> { + let original_id_for_error = shareholder.get_id(); + captured_db_for_set_shareholder + .set(&shareholder) + .map(|(_id, updated_shareholder)| updated_shareholder) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Shareholder (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_sh = Arc::clone(&db); - engine.register_fn("get_shareholder_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_sh.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Shareholder with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_shareholder_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_sh + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Shareholder with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // Mock DB functions for Company let captured_db_for_set_company = Arc::clone(&db); - engine.register_fn("set_company", move |company: Company| -> Result> { - let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB - captured_db_for_set_company.set(&company) - .map(|(_id, updated_company)| updated_company) // Use the model returned by db.set() - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Company (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_company", + move |company: Company| -> Result> { + let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB + captured_db_for_set_company + .set(&company) + .map(|(_id, updated_company)| updated_company) // Use the model returned by db.set() + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Company (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get = Arc::clone(&db); - engine.register_fn("get_company_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow - captured_db_for_get.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Company (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Company with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_company_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow + captured_db_for_get + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Company (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Company with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); + + // --- Payment --- + engine.register_type_with_name::("Payment"); + + // Constructor for Payment + engine.register_fn( + "new_payment", + |payment_intent_id: String, + company_id: i64, + payment_plan: String, + setup_fee: f64, + monthly_fee: f64, + total_amount: f64| + -> Result> { + Ok(Payment::new( + payment_intent_id, + id_from_i64(company_id)?, + payment_plan, + setup_fee, + monthly_fee, + total_amount, + )) + }, + ); + + // Getters for Payment + engine.register_get( + "id", + |payment: &mut Payment| -> Result> { Ok(payment.get_id() as i64) }, + ); + engine.register_get( + "created_at", + |payment: &mut Payment| -> Result> { + Ok(payment.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |payment: &mut Payment| -> Result> { + Ok(payment.base_data.modified_at) + }, + ); + engine.register_get( + "payment_intent_id", + |payment: &mut Payment| -> Result> { + Ok(payment.payment_intent_id.clone()) + }, + ); + engine.register_get( + "company_id", + |payment: &mut Payment| -> Result> { + Ok(payment.company_id as i64) + }, + ); + engine.register_get( + "payment_plan", + |payment: &mut Payment| -> Result> { + Ok(payment.payment_plan.clone()) + }, + ); + engine.register_get( + "setup_fee", + |payment: &mut Payment| -> Result> { Ok(payment.setup_fee) }, + ); + engine.register_get( + "monthly_fee", + |payment: &mut Payment| -> Result> { Ok(payment.monthly_fee) }, + ); + engine.register_get( + "total_amount", + |payment: &mut Payment| -> Result> { Ok(payment.total_amount) }, + ); + engine.register_get( + "status", + |payment: &mut Payment| -> Result> { + Ok(payment.status.clone()) + }, + ); + engine.register_get( + "stripe_customer_id", + |payment: &mut Payment| -> Result, Box> { + Ok(payment.stripe_customer_id.clone()) + }, + ); + + // Builder methods for Payment + engine.register_fn( + "payment_intent_id", + |payment: Payment, payment_intent_id: String| -> Result> { + Ok(payment.payment_intent_id(payment_intent_id)) + }, + ); + engine.register_fn( + "company_id", + |payment: Payment, company_id: i64| -> Result> { + Ok(payment.company_id(id_from_i64(company_id)?)) + }, + ); + engine.register_fn( + "payment_plan", + |payment: Payment, payment_plan: String| -> Result> { + Ok(payment.payment_plan(payment_plan)) + }, + ); + engine.register_fn( + "setup_fee", + |payment: Payment, setup_fee: f64| -> Result> { + Ok(payment.setup_fee(setup_fee)) + }, + ); + engine.register_fn( + "monthly_fee", + |payment: Payment, monthly_fee: f64| -> Result> { + Ok(payment.monthly_fee(monthly_fee)) + }, + ); + engine.register_fn( + "total_amount", + |payment: Payment, total_amount: f64| -> Result> { + Ok(payment.total_amount(total_amount)) + }, + ); + engine.register_fn( + "status", + |payment: Payment, status: PaymentStatus| -> Result> { + Ok(payment.status(status)) + }, + ); + engine.register_fn( + "stripe_customer_id", + |payment: Payment, + stripe_customer_id: Option| + -> Result> { + Ok(payment.stripe_customer_id(stripe_customer_id)) + }, + ); + + // Business logic methods for Payment + engine.register_fn( + "complete_payment", + |payment: Payment, + stripe_customer_id: Option| + -> Result> { + Ok(payment.complete_payment(stripe_customer_id)) + }, + ); + // Overload for string parameter + engine.register_fn( + "complete_payment", + |payment: Payment, stripe_customer_id: String| -> Result> { + Ok(payment.complete_payment(Some(stripe_customer_id))) + }, + ); + engine.register_fn( + "fail_payment", + |payment: Payment| -> Result> { Ok(payment.fail_payment()) }, + ); + engine.register_fn( + "refund_payment", + |payment: Payment| -> Result> { Ok(payment.refund_payment()) }, + ); + + // Status check methods for Payment + engine.register_fn( + "is_completed", + |payment: &mut Payment| -> Result> { Ok(payment.is_completed()) }, + ); + engine.register_fn( + "is_pending", + |payment: &mut Payment| -> Result> { Ok(payment.is_pending()) }, + ); + engine.register_fn( + "has_failed", + |payment: &mut Payment| -> Result> { Ok(payment.has_failed()) }, + ); + engine.register_fn( + "is_refunded", + |payment: &mut Payment| -> Result> { Ok(payment.is_refunded()) }, + ); + + // DB functions for Payment + let captured_db_for_set_payment = Arc::clone(&db); + engine.register_fn( + "set_payment", + move |payment: Payment| -> Result> { + let original_id_for_error = payment.get_id(); + captured_db_for_set_payment + .set(&payment) + .map(|(_id, updated_payment)| updated_payment) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Payment (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); + + let captured_db_for_get_payment = Arc::clone(&db); + engine.register_fn( + "get_payment_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_from_i64(id_i64)?; + captured_db_for_get_payment + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Payment (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Payment with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); engine.register_global_module(module.into()); } diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index 5145503..850f48c 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -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}; diff --git a/heromodels/tests/payment.rs b/heromodels/tests/payment.rs new file mode 100644 index 0000000..04331ec --- /dev/null +++ b/heromodels/tests/payment.rs @@ -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 { + 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()); +}