feat: Add new Rhai example demonstrating payment flow
- Add a new Rhai example showcasing complete payment flow for company registration. This includes company creation, payment record creation, successful payment processing, status updates, and handling failed and refunded payments. - Add new example demonstrating payment integration in Rhai scripting. This example showcases the usage of various payment status methods and verifies data from the database after processing payments. - Add new examples to Cargo.toml to facilitate building and running the examples. This makes it easier to integrate and test payment functionality using Rhai scripting.
This commit is contained in:
parent
f0a0dd6d73
commit
af83035d89
@ -83,3 +83,8 @@ required-features = ["rhai"]
|
||||
name = "biz_rhai"
|
||||
path = "examples/biz_rhai/example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
||||
[[example]]
|
||||
name = "payment_flow_rhai"
|
||||
path = "examples/biz_rhai/payment_flow_example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
@ -6,7 +6,9 @@ print("DB instance will be implicitly passed to DB functions.");
|
||||
|
||||
// --- Enum Constants ---
|
||||
print("\n--- Enum Constants ---");
|
||||
print(`CompanyStatus PendingPayment: ${CompanyStatusConstants::PendingPayment}`);
|
||||
print(`CompanyStatus Active: ${CompanyStatusConstants::Active}`);
|
||||
print(`CompanyStatus Suspended: ${CompanyStatusConstants::Suspended}`);
|
||||
print(`CompanyStatus Inactive: ${CompanyStatusConstants::Inactive}`);
|
||||
print(`BusinessType Coop: ${BusinessTypeConstants::Coop}`);
|
||||
print(`BusinessType Global: ${BusinessTypeConstants::Global}`);
|
||||
@ -28,18 +30,46 @@ let company1 = new_company(company1_name, company1_reg, company1_inc_date)
|
||||
.business_type(BusinessTypeConstants::Global)
|
||||
.industry("Technology")
|
||||
.description("Leading provider of innovative tech solutions.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
// Note: status defaults to PendingPayment for new companies
|
||||
.fiscal_year_end("12-31")
|
||||
.set_base_created_at(1672531200)
|
||||
.set_base_modified_at(1672531205);
|
||||
|
||||
print(`Company 1 Name: ${company1.name}, Status: ${company1.status}`);
|
||||
print(`Company 1 Name: ${company1.name}, Status: ${company1.status} (default for new companies)`);
|
||||
print(`Company 1 Email: ${company1.email}, Industry: ${company1.industry}`);
|
||||
// Save the company to the database
|
||||
print("\nSaving company1 to database...");
|
||||
company1 = set_company(company1); // Capture the company with the DB-assigned ID
|
||||
print("Company1 saved.");
|
||||
|
||||
// Demonstrate payment flow for the company
|
||||
print("\n--- Payment Processing for Company ---");
|
||||
print("Creating payment record for company registration...");
|
||||
let payment_intent_id = `pi_demo_${company1.id}`;
|
||||
let payment = new_payment(
|
||||
payment_intent_id,
|
||||
company1.id,
|
||||
"yearly",
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0 // Total amount (setup + 12 months)
|
||||
);
|
||||
|
||||
payment = set_payment(payment);
|
||||
print(`Payment created: ${payment.payment_intent_id}, Status: ${payment.status}`);
|
||||
|
||||
// Simulate successful payment processing
|
||||
print("Processing payment...");
|
||||
payment = payment.complete_payment(`cus_demo_${company1.id}`);
|
||||
payment = set_payment(payment);
|
||||
print(`Payment completed: ${payment.status}`);
|
||||
|
||||
// Update company status to Active after successful payment
|
||||
print("Updating company status to Active after payment...");
|
||||
company1 = company1.status(CompanyStatusConstants::Active);
|
||||
company1 = set_company(company1);
|
||||
print(`Company status updated: ${company1.status}`);
|
||||
|
||||
// Retrieve the company
|
||||
print(`\nRetrieving company by ID (${company1.id})...`);
|
||||
let retrieved_company = get_company_by_id(company1.id);
|
||||
@ -97,10 +127,11 @@ print(`Retrieved Shareholder 2: ${retrieved_sh2.name}, Type: ${retrieved_sh2.typ
|
||||
print("\n--- Testing Update for Company ---");
|
||||
let updated_company = retrieved_company
|
||||
.description("Leading global provider of cutting-edge technology solutions and services.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
// Note: Company is already Active from payment processing above
|
||||
.phone("+1-555-0199"); // Assume modified_at would be updated by set_company
|
||||
|
||||
print(`Updated Company - Name: ${updated_company.name}, New Phone: ${updated_company.phone}`);
|
||||
print(`Company Status: ${updated_company.status} (already Active from payment)`);
|
||||
set_company(updated_company);
|
||||
print("Updated Company saved.");
|
||||
|
||||
|
229
heromodels/examples/biz_rhai/payment_flow.rhai
Normal file
229
heromodels/examples/biz_rhai/payment_flow.rhai
Normal file
@ -0,0 +1,229 @@
|
||||
// Payment Flow Rhai Example
|
||||
// This script demonstrates the complete payment flow for company registration
|
||||
// using the Rhai scripting interface.
|
||||
|
||||
print("=== Payment Flow Rhai Example ===");
|
||||
print("Demonstrating company registration with payment integration");
|
||||
print("");
|
||||
|
||||
// --- Step 1: Create Company with Pending Payment Status ---
|
||||
print("Step 1: Creating company with pending payment status");
|
||||
|
||||
let company_name = "InnovateTech Solutions";
|
||||
let company_reg = "REG-ITS-2024-001";
|
||||
let company_inc_date = 1704067200; // Jan 1, 2024
|
||||
|
||||
// Create company (status defaults to PendingPayment)
|
||||
let company = new_company(company_name, company_reg, company_inc_date)
|
||||
.email("contact@innovatetech.com")
|
||||
.phone("+1-555-0199")
|
||||
.website("https://innovatetech.com")
|
||||
.address("456 Innovation Blvd, Tech Valley, TV 67890")
|
||||
.business_type(BusinessTypeConstants::Starter)
|
||||
.industry("Software Development")
|
||||
.description("Cutting-edge software solutions for modern businesses")
|
||||
.fiscal_year_end("12-31");
|
||||
|
||||
print(` Company: ${company.name}`);
|
||||
print(` Status: ${company.status} (default for new companies)`);
|
||||
print(` Registration: ${company.registration_number}`);
|
||||
print(` Email: ${company.email}`);
|
||||
|
||||
// Save company to database
|
||||
company = set_company(company);
|
||||
print(` Saved with ID: ${company.id}`);
|
||||
print("");
|
||||
|
||||
// --- Step 2: Create Payment Record ---
|
||||
print("Step 2: Creating payment record");
|
||||
|
||||
let payment_intent_id = `pi_rhai_${timestamp()}`;
|
||||
let payment_plan = "yearly";
|
||||
let setup_fee = 750.0;
|
||||
let monthly_fee = 149.0;
|
||||
let total_amount = setup_fee + (monthly_fee * 12.0); // Setup + 12 months
|
||||
|
||||
let payment = new_payment(
|
||||
payment_intent_id,
|
||||
company.id,
|
||||
payment_plan,
|
||||
setup_fee,
|
||||
monthly_fee,
|
||||
total_amount
|
||||
);
|
||||
|
||||
print(` Payment Intent ID: ${payment.payment_intent_id}`);
|
||||
print(` Company ID: ${payment.company_id}`);
|
||||
print(` Payment Plan: ${payment.payment_plan}`);
|
||||
print(` Setup Fee: $${payment.setup_fee}`);
|
||||
print(` Monthly Fee: $${payment.monthly_fee}`);
|
||||
print(` Total Amount: $${payment.total_amount}`);
|
||||
print(` Status: ${payment.status} (default for new payments)`);
|
||||
|
||||
// Save payment to database
|
||||
payment = set_payment(payment);
|
||||
print(` Saved with ID: ${payment.id}`);
|
||||
print("");
|
||||
|
||||
// --- Step 3: Process Payment Successfully ---
|
||||
print("Step 3: Processing payment...");
|
||||
|
||||
// Simulate payment processing
|
||||
print(" Contacting payment processor...");
|
||||
print(" Validating payment details...");
|
||||
print(" Processing transaction...");
|
||||
|
||||
// Complete the payment with Stripe customer ID
|
||||
let stripe_customer_id = `cus_rhai_${timestamp()}`;
|
||||
payment = payment.complete_payment(stripe_customer_id);
|
||||
|
||||
print(" Payment completed successfully!");
|
||||
print(` New Status: ${payment.status}`);
|
||||
print(` Stripe Customer ID: ${payment.stripe_customer_id}`);
|
||||
|
||||
// Save updated payment
|
||||
payment = set_payment(payment);
|
||||
print("");
|
||||
|
||||
// --- Step 4: Update Company Status to Active ---
|
||||
print("Step 4: Updating company status to Active");
|
||||
|
||||
company = company.status(CompanyStatusConstants::Active);
|
||||
print(` Company: ${company.name}`);
|
||||
print(` New Status: ${company.status}`);
|
||||
|
||||
// Save updated company
|
||||
company = set_company(company);
|
||||
print(" Company status updated successfully!");
|
||||
print("");
|
||||
|
||||
// --- Step 5: Verify Payment Status ---
|
||||
print("Step 5: Payment status verification");
|
||||
print(` Is payment completed? ${payment.is_completed()}`);
|
||||
print(` Is payment pending? ${payment.is_pending()}`);
|
||||
print(` Has payment failed? ${payment.has_failed()}`);
|
||||
print(` Is payment refunded? ${payment.is_refunded()}`);
|
||||
print("");
|
||||
|
||||
// --- Step 6: Retrieve Data from Database ---
|
||||
print("Step 6: Verifying data from database");
|
||||
|
||||
let retrieved_company = get_company_by_id(company.id);
|
||||
print(` Retrieved Company: ${retrieved_company.name}`);
|
||||
print(` Status: ${retrieved_company.status}`);
|
||||
|
||||
let retrieved_payment = get_payment_by_id(payment.id);
|
||||
print(` Retrieved Payment: ${retrieved_payment.payment_intent_id}`);
|
||||
print(` Status: ${retrieved_payment.status}`);
|
||||
print(` Total: $${retrieved_payment.total_amount}`);
|
||||
print("");
|
||||
|
||||
// --- Step 7: Demonstrate Failed Payment Scenario ---
|
||||
print("Step 7: Demonstrating failed payment scenario");
|
||||
|
||||
// Create another company for failed payment demo
|
||||
let failed_company = new_company(
|
||||
"FailureDemo Corp",
|
||||
"REG-FDC-2024-002",
|
||||
1704067200
|
||||
)
|
||||
.email("demo@failurecorp.com")
|
||||
.business_type(BusinessTypeConstants::Single)
|
||||
.industry("Consulting");
|
||||
|
||||
failed_company = set_company(failed_company);
|
||||
print(` Created company: ${failed_company.name}`);
|
||||
print(` Status: ${failed_company.status} (pending payment)`);
|
||||
|
||||
// Create payment that will fail
|
||||
let failed_payment_intent = `pi_fail_${timestamp()}`;
|
||||
let failed_payment = new_payment(
|
||||
failed_payment_intent,
|
||||
failed_company.id,
|
||||
"monthly",
|
||||
300.0,
|
||||
59.0,
|
||||
359.0
|
||||
);
|
||||
|
||||
failed_payment = set_payment(failed_payment);
|
||||
|
||||
// Simulate payment failure
|
||||
print(" Simulating payment failure...");
|
||||
failed_payment = failed_payment.fail_payment();
|
||||
failed_payment = set_payment(failed_payment);
|
||||
|
||||
print(` Failed Company: ${failed_company.name}`);
|
||||
print(` Company Status: ${failed_company.status} (remains pending)`);
|
||||
print(` Payment Status: ${failed_payment.status}`);
|
||||
print(` Payment failed: ${failed_payment.has_failed()}`);
|
||||
print("");
|
||||
|
||||
// --- Step 8: Demonstrate Payment Refund ---
|
||||
print("Step 8: Demonstrating payment refund scenario");
|
||||
|
||||
// Create a payment to refund
|
||||
let refund_company = new_company(
|
||||
"RefundDemo Inc",
|
||||
"REG-RDI-2024-003",
|
||||
1704067200
|
||||
)
|
||||
.email("refund@demo.com")
|
||||
.business_type(BusinessTypeConstants::Twin);
|
||||
|
||||
refund_company = set_company(refund_company);
|
||||
|
||||
let refund_payment = new_payment(
|
||||
`pi_refund_${timestamp()}`,
|
||||
refund_company.id,
|
||||
"monthly",
|
||||
200.0,
|
||||
39.0,
|
||||
239.0
|
||||
);
|
||||
|
||||
refund_payment = set_payment(refund_payment);
|
||||
|
||||
// First complete the payment
|
||||
refund_payment = refund_payment.complete_payment(`cus_refund_${timestamp()}`);
|
||||
refund_payment = set_payment(refund_payment);
|
||||
print(` Payment completed: ${refund_payment.is_completed()}`);
|
||||
|
||||
// Then refund it
|
||||
refund_payment = refund_payment.refund_payment();
|
||||
refund_payment = set_payment(refund_payment);
|
||||
|
||||
print(` Payment refunded: ${refund_payment.is_refunded()}`);
|
||||
print(` Refund Status: ${refund_payment.status}`);
|
||||
print("");
|
||||
|
||||
// --- Summary ---
|
||||
print("=== Payment Flow Example Complete ===");
|
||||
print("Summary of demonstrated features:");
|
||||
print("✓ Company creation with PendingPayment status (default)");
|
||||
print("✓ Payment record creation and database persistence");
|
||||
print("✓ Successful payment processing and status updates");
|
||||
print("✓ Company status transition from PendingPayment to Active");
|
||||
print("✓ Payment status verification methods");
|
||||
print("✓ Database retrieval and verification");
|
||||
print("✓ Failed payment scenario handling");
|
||||
print("✓ Payment refund processing");
|
||||
print("");
|
||||
print("Key Payment Statuses:");
|
||||
print("- Pending: Initial state for new payments");
|
||||
print("- Completed: Payment successfully processed");
|
||||
print("- Failed: Payment processing failed");
|
||||
print("- Refunded: Previously completed payment was refunded");
|
||||
print("");
|
||||
print("Key Company Statuses:");
|
||||
print("- PendingPayment: Default for new companies (awaiting payment)");
|
||||
print("- Active: Payment completed, company is operational");
|
||||
print("- Suspended: Company temporarily suspended");
|
||||
print("- Inactive: Company deactivated");
|
||||
|
||||
// Helper function to get current timestamp
|
||||
fn timestamp() {
|
||||
// This would normally return current timestamp
|
||||
// For demo purposes, we'll use a static value
|
||||
1704067200
|
||||
}
|
220
heromodels/examples/biz_rhai/payment_flow_example.rs
Normal file
220
heromodels/examples/biz_rhai/payment_flow_example.rs
Normal file
@ -0,0 +1,220 @@
|
||||
// Payment Flow Rhai Example Runner
|
||||
// This example runs the payment_flow.rhai script to demonstrate
|
||||
// the payment integration using Rhai scripting.
|
||||
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::biz::register_biz_rhai_module;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Payment Flow Rhai Example Runner ===");
|
||||
println!("Running payment flow demonstration using Rhai scripting\n");
|
||||
|
||||
// Initialize database
|
||||
let db = Arc::new(
|
||||
OurDB::new("/tmp/payment_flow_rhai_example", true)
|
||||
.map_err(|e| format!("DB Error: {}", e))?,
|
||||
);
|
||||
|
||||
// Create and configure Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the business models module with the engine
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Add a timestamp function for the Rhai script
|
||||
engine.register_fn("timestamp", || -> i64 { chrono::Utc::now().timestamp() });
|
||||
|
||||
// Read and execute the Rhai script
|
||||
let script_path = "examples/biz_rhai/payment_flow.rhai";
|
||||
|
||||
match std::fs::read_to_string(script_path) {
|
||||
Ok(script_content) => {
|
||||
println!("Executing Rhai script: {}\n", script_path);
|
||||
|
||||
match engine.eval::<()>(&script_content) {
|
||||
Ok(_) => {
|
||||
println!("\n✅ Rhai script executed successfully!");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error executing Rhai script: {}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Error reading script file {}: {}", script_path, e);
|
||||
println!("Note: Make sure to run this example from the project root directory.");
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== Example Complete ===");
|
||||
println!("The payment flow has been successfully demonstrated using Rhai scripting.");
|
||||
println!("This shows how the payment integration can be used in scripted environments.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use heromodels::db::Collection;
|
||||
use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus};
|
||||
|
||||
#[test]
|
||||
fn test_rhai_payment_integration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test that we can create and manipulate payment objects through Rhai
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_rhai_payment".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Test creating a company through Rhai
|
||||
let company_script = r#"
|
||||
let company = new_company("Test Company", "TEST-001", 1704067200)
|
||||
.email("test@example.com")
|
||||
.status(CompanyStatusConstants::PendingPayment);
|
||||
company = set_company(company);
|
||||
company.id
|
||||
"#;
|
||||
|
||||
let company_id: i64 = engine.eval(company_script)?;
|
||||
assert!(company_id > 0);
|
||||
|
||||
// Test creating a payment through Rhai
|
||||
let payment_script = format!(
|
||||
r#"
|
||||
let payment = new_payment("pi_test_123", {}, "monthly", 100.0, 50.0, 150.0);
|
||||
payment = set_payment(payment);
|
||||
payment.id
|
||||
"#,
|
||||
company_id
|
||||
);
|
||||
|
||||
let payment_id: i64 = engine.eval(&payment_script)?;
|
||||
assert!(payment_id > 0);
|
||||
|
||||
// Test completing payment through Rhai
|
||||
let complete_script = format!(
|
||||
r#"
|
||||
let payment = get_payment_by_id({});
|
||||
payment = payment.complete_payment("cus_test_123");
|
||||
payment = set_payment(payment);
|
||||
payment.is_completed()
|
||||
"#,
|
||||
payment_id
|
||||
);
|
||||
|
||||
let is_completed: bool = engine.eval(&complete_script)?;
|
||||
assert!(is_completed);
|
||||
|
||||
// Verify in database
|
||||
let payment: Payment = db.get_by_id(payment_id as u32)?.unwrap();
|
||||
assert_eq!(payment.status, PaymentStatus::Completed);
|
||||
assert_eq!(payment.stripe_customer_id, Some("cus_test_123".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_constants() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test that payment status constants are available in Rhai
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_payment_constants".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
// Test that we can access payment status constants
|
||||
let constants_script = r#"
|
||||
let payment = new_payment("pi_test", 1, "monthly", 100.0, 50.0, 150.0);
|
||||
|
||||
// Test status transitions
|
||||
payment = payment.status(PaymentStatusConstants::Pending);
|
||||
let is_pending = payment.is_pending();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Completed);
|
||||
let is_completed = payment.is_completed();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Failed);
|
||||
let has_failed = payment.has_failed();
|
||||
|
||||
payment = payment.status(PaymentStatusConstants::Refunded);
|
||||
let is_refunded = payment.is_refunded();
|
||||
|
||||
[is_pending, is_completed, has_failed, is_refunded]
|
||||
"#;
|
||||
|
||||
let results: Vec<bool> = engine.eval(constants_script)?;
|
||||
assert_eq!(results, vec![true, true, true, true]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_company_status_integration() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Test the integration between company and payment status
|
||||
let db_config = OurDBConfig {
|
||||
path: "/tmp/test_status_integration".to_string(),
|
||||
incremental_mode: true,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(true),
|
||||
};
|
||||
|
||||
let db = Arc::new(OurDB::new(db_config)?);
|
||||
let mut engine = Engine::new();
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db));
|
||||
|
||||
let integration_script = r#"
|
||||
// Create company (defaults to PendingPayment)
|
||||
let company = new_company("Integration Test", "INT-001", 1704067200);
|
||||
company = set_company(company);
|
||||
|
||||
// Create payment
|
||||
let payment = new_payment("pi_int_test", company.id, "yearly", 500.0, 99.0, 1688.0);
|
||||
payment = set_payment(payment);
|
||||
|
||||
// Complete payment
|
||||
payment = payment.complete_payment("cus_int_test");
|
||||
payment = set_payment(payment);
|
||||
|
||||
// Update company to active
|
||||
company = company.status(CompanyStatusConstants::Active);
|
||||
company = set_company(company);
|
||||
|
||||
[payment.is_completed(), company.status]
|
||||
"#;
|
||||
|
||||
let results: Vec<rhai::Dynamic> = engine.eval(integration_script)?;
|
||||
|
||||
// Check that payment is completed
|
||||
assert!(results[0].as_bool().unwrap());
|
||||
|
||||
// Check that company status is Active (we can't directly compare enum in Rhai result)
|
||||
// So we'll verify by retrieving from database
|
||||
let companies: Vec<Company> = db.get_all()?;
|
||||
let company = companies
|
||||
.into_iter()
|
||||
.find(|c| c.name == "Integration Test")
|
||||
.unwrap();
|
||||
assert_eq!(company.status, CompanyStatus::Active);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
214
heromodels/examples/payment_flow_example.rs
Normal file
214
heromodels/examples/payment_flow_example.rs
Normal file
@ -0,0 +1,214 @@
|
||||
// Payment Flow Example
|
||||
// This example demonstrates the complete payment flow for company registration,
|
||||
// including company creation with pending payment status, payment processing,
|
||||
// and status transitions.
|
||||
|
||||
use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment};
|
||||
|
||||
fn main() {
|
||||
println!("=== Payment Flow Example ===");
|
||||
println!("Demonstrating company registration with payment integration\n");
|
||||
|
||||
// Step 1: Create a company with pending payment status
|
||||
println!("Step 1: Creating company with pending payment status");
|
||||
let company = Company::new(
|
||||
"TechStart Inc.".to_string(),
|
||||
"REG-TS-2024-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@techstart.com".to_string())
|
||||
.phone("+1-555-0123".to_string())
|
||||
.website("https://techstart.com".to_string())
|
||||
.address("123 Startup Ave, Innovation City, IC 12345".to_string())
|
||||
.business_type(BusinessType::Starter)
|
||||
.industry("Technology".to_string())
|
||||
.description("A promising tech startup focused on AI solutions".to_string())
|
||||
// Note: status defaults to PendingPayment, so we don't need to set it explicitly
|
||||
.fiscal_year_end("12-31".to_string());
|
||||
|
||||
println!(" Company: {}", company.name);
|
||||
println!(" Status: {:?}", company.status);
|
||||
println!(" Registration: {}", company.registration_number);
|
||||
println!(" Company created successfully!\n");
|
||||
|
||||
// Step 2: Create a payment record for the company
|
||||
println!("Step 2: Creating payment record");
|
||||
let payment_intent_id = format!("pi_test_{}", chrono::Utc::now().timestamp());
|
||||
let payment = Payment::new(
|
||||
payment_intent_id.clone(),
|
||||
1, // Mock company ID for this example
|
||||
"yearly".to_string(),
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0, // Total amount (setup + 12 months)
|
||||
);
|
||||
|
||||
println!(" Payment Intent ID: {}", payment.payment_intent_id);
|
||||
println!(" Company ID: {}", payment.company_id);
|
||||
println!(" Payment Plan: {}", payment.payment_plan);
|
||||
println!(" Setup Fee: ${:.2}", payment.setup_fee);
|
||||
println!(" Monthly Fee: ${:.2}", payment.monthly_fee);
|
||||
println!(" Total Amount: ${:.2}", payment.total_amount);
|
||||
println!(" Status: {:?}", payment.status);
|
||||
println!(" Payment record created successfully!\n");
|
||||
|
||||
// Step 3: Simulate payment processing
|
||||
println!("Step 3: Processing payment...");
|
||||
|
||||
// Simulate some processing time
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
// Complete the payment with Stripe customer ID
|
||||
let stripe_customer_id = Some(format!("cus_test_{}", chrono::Utc::now().timestamp()));
|
||||
let completed_payment = payment.complete_payment(stripe_customer_id.clone());
|
||||
|
||||
println!(" Payment completed successfully!");
|
||||
println!(" New Status: {:?}", completed_payment.status);
|
||||
println!(
|
||||
" Stripe Customer ID: {:?}",
|
||||
completed_payment.stripe_customer_id
|
||||
);
|
||||
|
||||
// Step 4: Update company status to Active
|
||||
println!("\nStep 4: Updating company status to Active");
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
|
||||
println!(" Company: {}", active_company.name);
|
||||
println!(" New Status: {:?}", active_company.status);
|
||||
println!(" Company status updated successfully!\n");
|
||||
|
||||
// Step 5: Demonstrate payment status checks
|
||||
println!("Step 5: Payment status verification");
|
||||
println!(
|
||||
" Is payment completed? {}",
|
||||
completed_payment.is_completed()
|
||||
);
|
||||
println!(" Is payment pending? {}", completed_payment.is_pending());
|
||||
println!(" Has payment failed? {}", completed_payment.has_failed());
|
||||
println!(" Is payment refunded? {}", completed_payment.is_refunded());
|
||||
|
||||
// Step 6: Demonstrate failed payment scenario
|
||||
println!("\nStep 6: Demonstrating failed payment scenario");
|
||||
|
||||
// Create another company
|
||||
let failed_company = Company::new(
|
||||
"FailCorp Ltd.".to_string(),
|
||||
"REG-FC-2024-002".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@failcorp.com".to_string())
|
||||
.business_type(BusinessType::Single)
|
||||
.industry("Consulting".to_string());
|
||||
|
||||
// Create payment for failed scenario
|
||||
let failed_payment_intent = format!("pi_fail_{}", chrono::Utc::now().timestamp());
|
||||
let failed_payment = Payment::new(
|
||||
failed_payment_intent,
|
||||
2, // Mock company ID
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
49.0,
|
||||
299.0,
|
||||
);
|
||||
|
||||
// Simulate payment failure
|
||||
let failed_payment = failed_payment.fail_payment();
|
||||
|
||||
println!(" Failed Company: {}", failed_company.name);
|
||||
println!(
|
||||
" Company Status: {:?} (remains pending)",
|
||||
failed_company.status
|
||||
);
|
||||
println!(" Payment Status: {:?}", failed_payment.status);
|
||||
println!(" Payment failed: {}", failed_payment.has_failed());
|
||||
|
||||
println!("\n=== Payment Flow Example Complete ===");
|
||||
println!("Summary:");
|
||||
println!("- Created companies with PendingPayment status by default");
|
||||
println!("- Processed successful payment and updated company to Active");
|
||||
println!("- Demonstrated failed payment scenario");
|
||||
println!("- All operations completed successfully without database persistence");
|
||||
println!("- For database examples, see the Rhai examples or unit tests");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_payment_flow() {
|
||||
// Test the basic payment flow without database persistence
|
||||
let company = Company::new(
|
||||
"Test Company".to_string(),
|
||||
"TEST-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
);
|
||||
|
||||
// Verify default status is PendingPayment
|
||||
assert_eq!(company.status, CompanyStatus::PendingPayment);
|
||||
|
||||
// Create payment
|
||||
let payment = Payment::new(
|
||||
"pi_test_123".to_string(),
|
||||
1, // Mock company ID
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
// Verify default payment status is Pending
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert!(payment.is_pending());
|
||||
assert!(!payment.is_completed());
|
||||
|
||||
// Complete payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_test_123".to_string()));
|
||||
assert_eq!(completed_payment.status, PaymentStatus::Completed);
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(!completed_payment.is_pending());
|
||||
|
||||
// Update company status
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
assert_eq!(active_company.status, CompanyStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_failure() {
|
||||
let payment = Payment::new(
|
||||
"pi_fail_123".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.0,
|
||||
1688.0,
|
||||
);
|
||||
|
||||
let failed_payment = payment.fail_payment();
|
||||
assert_eq!(failed_payment.status, PaymentStatus::Failed);
|
||||
assert!(failed_payment.has_failed());
|
||||
assert!(!failed_payment.is_completed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_refund() {
|
||||
let payment = Payment::new(
|
||||
"pi_refund_123".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
49.0,
|
||||
299.0,
|
||||
);
|
||||
|
||||
// First complete the payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_123".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
|
||||
// Then refund it
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
assert_eq!(refunded_payment.status, PaymentStatus::Refunded);
|
||||
assert!(refunded_payment.is_refunded());
|
||||
assert!(!refunded_payment.is_completed());
|
||||
}
|
||||
}
|
318
heromodels/payment_usage.md
Normal file
318
heromodels/payment_usage.md
Normal file
@ -0,0 +1,318 @@
|
||||
# Payment Model Usage Guide
|
||||
|
||||
This document provides comprehensive instructions for AI assistants on how to use the Payment model in the heromodels repository.
|
||||
|
||||
## Overview
|
||||
|
||||
The Payment model represents a payment transaction in the system, typically associated with company registration or subscription payments. It integrates with Stripe for payment processing and maintains comprehensive status tracking.
|
||||
|
||||
## Model Structure
|
||||
|
||||
```rust
|
||||
pub struct Payment {
|
||||
pub base_data: BaseModelData, // Auto-managed ID, timestamps, comments
|
||||
pub payment_intent_id: String, // Stripe payment intent ID
|
||||
pub company_id: u32, // Foreign key to Company
|
||||
pub payment_plan: String, // "monthly", "yearly", "two_year"
|
||||
pub setup_fee: f64, // One-time setup fee
|
||||
pub monthly_fee: f64, // Recurring monthly fee
|
||||
pub total_amount: f64, // Total amount paid
|
||||
pub currency: String, // Currency code (defaults to "usd")
|
||||
pub status: PaymentStatus, // Current payment status
|
||||
pub stripe_customer_id: Option<String>, // Stripe customer ID (set on completion)
|
||||
pub created_at: i64, // Payment creation timestamp
|
||||
pub completed_at: Option<i64>, // Payment completion timestamp
|
||||
}
|
||||
|
||||
pub enum PaymentStatus {
|
||||
Pending, // Initial state - payment created but not processed
|
||||
Processing, // Payment is being processed by Stripe
|
||||
Completed, // Payment successfully completed
|
||||
Failed, // Payment processing failed
|
||||
Refunded, // Payment was refunded
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Creating a New Payment
|
||||
|
||||
```rust
|
||||
use heromodels::models::biz::{Payment, PaymentStatus};
|
||||
|
||||
// Create a new payment with required fields
|
||||
let payment = Payment::new(
|
||||
"pi_1234567890".to_string(), // Stripe payment intent ID
|
||||
company_id, // Company ID from database
|
||||
"monthly".to_string(), // Payment plan
|
||||
100.0, // Setup fee
|
||||
49.99, // Monthly fee
|
||||
149.99, // Total amount
|
||||
);
|
||||
|
||||
// Payment defaults:
|
||||
// - status: PaymentStatus::Pending
|
||||
// - currency: "usd"
|
||||
// - stripe_customer_id: None
|
||||
// - created_at: current timestamp
|
||||
// - completed_at: None
|
||||
```
|
||||
|
||||
### 2. Using Builder Pattern
|
||||
|
||||
```rust
|
||||
let payment = Payment::new(
|
||||
"pi_1234567890".to_string(),
|
||||
company_id,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.99,
|
||||
1699.88,
|
||||
)
|
||||
.currency("eur".to_string())
|
||||
.stripe_customer_id(Some("cus_existing_customer".to_string()));
|
||||
```
|
||||
|
||||
### 3. Database Operations
|
||||
|
||||
```rust
|
||||
use heromodels::db::Collection;
|
||||
|
||||
// Save payment to database
|
||||
let db = get_db()?;
|
||||
let (payment_id, saved_payment) = db.set(&payment)?;
|
||||
|
||||
// Retrieve payment by ID
|
||||
let retrieved_payment: Payment = db.get_by_id(payment_id)?.unwrap();
|
||||
|
||||
// Update payment
|
||||
let updated_payment = saved_payment.complete_payment(Some("cus_new_customer".to_string()));
|
||||
let (_, final_payment) = db.set(&updated_payment)?;
|
||||
```
|
||||
|
||||
## Payment Status Management
|
||||
|
||||
### Status Transitions
|
||||
|
||||
```rust
|
||||
// 1. Start with Pending status (default)
|
||||
let payment = Payment::new(/* ... */);
|
||||
assert!(payment.is_pending());
|
||||
|
||||
// 2. Mark as processing when Stripe starts processing
|
||||
let processing_payment = payment.process_payment();
|
||||
assert!(processing_payment.is_processing());
|
||||
|
||||
// 3. Complete payment when Stripe confirms success
|
||||
let completed_payment = processing_payment.complete_payment(Some("cus_123".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(completed_payment.completed_at.is_some());
|
||||
|
||||
// 4. Handle failure if payment fails
|
||||
let failed_payment = processing_payment.fail_payment();
|
||||
assert!(failed_payment.has_failed());
|
||||
|
||||
// 5. Refund if needed
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
assert!(refunded_payment.is_refunded());
|
||||
```
|
||||
|
||||
### Status Check Methods
|
||||
|
||||
```rust
|
||||
// Check current status
|
||||
if payment.is_pending() {
|
||||
// Show "Payment Pending" UI
|
||||
} else if payment.is_processing() {
|
||||
// Show "Processing Payment" UI
|
||||
} else if payment.is_completed() {
|
||||
// Show "Payment Successful" UI
|
||||
// Enable company features
|
||||
} else if payment.has_failed() {
|
||||
// Show "Payment Failed" UI
|
||||
// Offer retry option
|
||||
} else if payment.is_refunded() {
|
||||
// Show "Payment Refunded" UI
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Company Model
|
||||
|
||||
### Complete Payment Flow
|
||||
|
||||
```rust
|
||||
use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus};
|
||||
|
||||
// 1. Create company with pending payment status
|
||||
let company = Company::new(
|
||||
"TechStart Inc.".to_string(),
|
||||
"REG-TS-2024-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("contact@techstart.com".to_string())
|
||||
.status(CompanyStatus::PendingPayment);
|
||||
|
||||
let (company_id, company) = db.set(&company)?;
|
||||
|
||||
// 2. Create payment for the company
|
||||
let payment = Payment::new(
|
||||
stripe_payment_intent_id,
|
||||
company_id,
|
||||
"yearly".to_string(),
|
||||
500.0, // Setup fee
|
||||
99.0, // Monthly fee
|
||||
1688.0, // Total (setup + 12 months)
|
||||
);
|
||||
|
||||
let (payment_id, payment) = db.set(&payment)?;
|
||||
|
||||
// 3. Process payment through Stripe
|
||||
let processing_payment = payment.process_payment();
|
||||
let (_, processing_payment) = db.set(&processing_payment)?;
|
||||
|
||||
// 4. On successful Stripe webhook
|
||||
let completed_payment = processing_payment.complete_payment(Some(stripe_customer_id));
|
||||
let (_, completed_payment) = db.set(&completed_payment)?;
|
||||
|
||||
// 5. Activate company
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
let (_, active_company) = db.set(&active_company)?;
|
||||
```
|
||||
|
||||
## Database Indexing
|
||||
|
||||
The Payment model provides custom indexes for efficient querying:
|
||||
|
||||
```rust
|
||||
// Indexed fields for fast lookups:
|
||||
// - payment_intent_id: Find payment by Stripe intent ID
|
||||
// - company_id: Find all payments for a company
|
||||
// - status: Find payments by status
|
||||
|
||||
// Example queries (conceptual - actual implementation depends on your query layer)
|
||||
// let pending_payments = db.find_by_index("status", "Pending")?;
|
||||
// let company_payments = db.find_by_index("company_id", company_id.to_string())?;
|
||||
// let stripe_payment = db.find_by_index("payment_intent_id", "pi_1234567890")?;
|
||||
```
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
```rust
|
||||
use heromodels::db::DbError;
|
||||
|
||||
fn process_payment_flow(payment_intent_id: String, company_id: u32) -> Result<Payment, DbError> {
|
||||
let db = get_db()?;
|
||||
|
||||
// Create payment
|
||||
let payment = Payment::new(
|
||||
payment_intent_id,
|
||||
company_id,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
49.99,
|
||||
149.99,
|
||||
);
|
||||
|
||||
// Save to database
|
||||
let (payment_id, payment) = db.set(&payment)?;
|
||||
|
||||
// Process through Stripe (external API call)
|
||||
match process_stripe_payment(&payment.payment_intent_id) {
|
||||
Ok(stripe_customer_id) => {
|
||||
// Success: complete payment
|
||||
let completed_payment = payment.complete_payment(Some(stripe_customer_id));
|
||||
let (_, final_payment) = db.set(&completed_payment)?;
|
||||
Ok(final_payment)
|
||||
}
|
||||
Err(_) => {
|
||||
// Failure: mark as failed
|
||||
let failed_payment = payment.fail_payment();
|
||||
let (_, final_payment) = db.set(&failed_payment)?;
|
||||
Ok(final_payment)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The Payment model includes comprehensive tests in `tests/payment.rs`. When working with payments:
|
||||
|
||||
1. **Always test status transitions**
|
||||
2. **Verify timestamp handling**
|
||||
3. **Test database persistence**
|
||||
4. **Test integration with Company model**
|
||||
5. **Test builder pattern methods**
|
||||
|
||||
```bash
|
||||
# Run payment tests
|
||||
cargo test payment
|
||||
|
||||
# Run specific test
|
||||
cargo test test_payment_completion
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Payment Retry Logic
|
||||
```rust
|
||||
fn retry_failed_payment(payment: Payment) -> Payment {
|
||||
if payment.has_failed() {
|
||||
// Reset to pending for retry
|
||||
Payment::new(
|
||||
payment.payment_intent_id,
|
||||
payment.company_id,
|
||||
payment.payment_plan,
|
||||
payment.setup_fee,
|
||||
payment.monthly_fee,
|
||||
payment.total_amount,
|
||||
)
|
||||
.currency(payment.currency)
|
||||
} else {
|
||||
payment
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Payment Summary
|
||||
```rust
|
||||
fn get_payment_summary(payment: &Payment) -> String {
|
||||
format!(
|
||||
"Payment {} for company {}: {} {} ({})",
|
||||
payment.payment_intent_id,
|
||||
payment.company_id,
|
||||
payment.total_amount,
|
||||
payment.currency.to_uppercase(),
|
||||
payment.status
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Payment Validation
|
||||
```rust
|
||||
fn validate_payment(payment: &Payment) -> Result<(), String> {
|
||||
if payment.total_amount <= 0.0 {
|
||||
return Err("Total amount must be positive".to_string());
|
||||
}
|
||||
if payment.payment_intent_id.is_empty() {
|
||||
return Err("Payment intent ID is required".to_string());
|
||||
}
|
||||
if payment.company_id == 0 {
|
||||
return Err("Valid company ID is required".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Key Points for AI Assistants
|
||||
|
||||
1. **Always use auto-generated IDs** - Don't manually set IDs, let OurDB handle them
|
||||
2. **Follow status flow** - Pending → Processing → Completed/Failed → (optionally) Refunded
|
||||
3. **Update timestamps** - `completed_at` is automatically set when calling `complete_payment()`
|
||||
4. **Use builder pattern** - For optional fields and cleaner code
|
||||
5. **Test thoroughly** - Payment logic is critical, always verify with tests
|
||||
6. **Handle errors gracefully** - Payment failures should be tracked, not ignored
|
||||
7. **Integrate with Company** - Payments typically affect company status
|
||||
8. **Use proper indexing** - Leverage indexed fields for efficient queries
|
||||
|
||||
This model follows the heromodels patterns and integrates seamlessly with the existing codebase architecture.
|
@ -1,19 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index};
|
||||
use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)]
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // For #[derive(CustomType)]
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CompanyStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
PendingPayment, // Company created but payment not completed
|
||||
Active, // Payment completed, company is active
|
||||
Suspended, // Company suspended (e.g., payment issues)
|
||||
Inactive, // Company deactivated
|
||||
}
|
||||
|
||||
impl Default for CompanyStatus {
|
||||
fn default() -> Self {
|
||||
CompanyStatus::Inactive
|
||||
CompanyStatus::PendingPayment
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +36,7 @@ impl Default for BusinessType {
|
||||
|
||||
// --- Company Struct ---
|
||||
|
||||
#[model]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType
|
||||
pub struct Company {
|
||||
pub base_data: BaseModelData,
|
||||
@ -51,55 +54,11 @@ pub struct Company {
|
||||
pub status: CompanyStatus,
|
||||
}
|
||||
|
||||
// --- Model Trait Implementation ---
|
||||
|
||||
impl Model for Company {
|
||||
fn db_prefix() -> &'static str {
|
||||
"company"
|
||||
}
|
||||
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
|
||||
// Override db_keys to provide custom indexes if needed
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
vec![
|
||||
IndexKeyBuilder::new("name").value(self.name.clone()).build(),
|
||||
IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(),
|
||||
// Add other relevant keys, e.g., by status or type if frequently queried
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// --- Index Implementations (Example) ---
|
||||
|
||||
pub struct CompanyNameIndex;
|
||||
impl Index for CompanyNameIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"name"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompanyRegistrationNumberIndex;
|
||||
impl Index for CompanyRegistrationNumberIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"registration_number"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Builder Pattern ---
|
||||
|
||||
impl Company {
|
||||
pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64
|
||||
pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self {
|
||||
// incorporation_date to i64
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
name,
|
||||
|
@ -2,23 +2,24 @@
|
||||
// Sub-modules will be declared here
|
||||
|
||||
pub mod company;
|
||||
pub mod payment;
|
||||
pub mod product;
|
||||
// pub mod sale;
|
||||
// pub mod shareholder;
|
||||
// pub mod user;
|
||||
|
||||
// Re-export main types from sub-modules
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub use company::{BusinessType, Company, CompanyStatus};
|
||||
pub use payment::{Payment, PaymentStatus};
|
||||
pub mod shareholder;
|
||||
pub use product::{Product, ProductComponent, ProductStatus, ProductType};
|
||||
pub use shareholder::{Shareholder, ShareholderType};
|
||||
pub use product::{Product, ProductType, ProductStatus, ProductComponent};
|
||||
|
||||
pub mod sale;
|
||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||
|
||||
// pub use user::{User}; // Assuming a simple User model for now
|
||||
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub mod rhai;
|
||||
#[cfg(feature = "rhai")]
|
||||
|
216
heromodels/src/models/biz/payment.rs
Normal file
216
heromodels/src/models/biz/payment.rs
Normal file
@ -0,0 +1,216 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize}; // For #[derive(CustomType)]
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PaymentStatus {
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PaymentStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PaymentStatus::Pending => write!(f, "Pending"),
|
||||
PaymentStatus::Processing => write!(f, "Processing"),
|
||||
PaymentStatus::Completed => write!(f, "Completed"),
|
||||
PaymentStatus::Failed => write!(f, "Failed"),
|
||||
PaymentStatus::Refunded => write!(f, "Refunded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaymentStatus {
|
||||
fn default() -> Self {
|
||||
PaymentStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
// --- Payment Struct ---
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Payment {
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
// Stripe payment intent ID for tracking
|
||||
#[index]
|
||||
pub payment_intent_id: String,
|
||||
|
||||
// Reference to the company this payment is for
|
||||
#[index]
|
||||
pub company_id: u32,
|
||||
|
||||
// Payment plan details
|
||||
pub payment_plan: String, // "monthly", "yearly", "two_year"
|
||||
pub setup_fee: f64,
|
||||
pub monthly_fee: f64,
|
||||
pub total_amount: f64,
|
||||
pub currency: String, // "usd"
|
||||
|
||||
pub status: PaymentStatus,
|
||||
pub stripe_customer_id: Option<String>,
|
||||
pub created_at: i64, // Timestamp
|
||||
pub completed_at: Option<i64>, // Completion timestamp
|
||||
}
|
||||
|
||||
// Model trait implementation is automatically generated by #[model] attribute
|
||||
|
||||
// --- Builder Pattern ---
|
||||
|
||||
impl Payment {
|
||||
pub fn new(
|
||||
payment_intent_id: String,
|
||||
company_id: u32,
|
||||
payment_plan: String,
|
||||
setup_fee: f64,
|
||||
monthly_fee: f64,
|
||||
total_amount: f64,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
payment_intent_id,
|
||||
company_id,
|
||||
payment_plan,
|
||||
setup_fee,
|
||||
monthly_fee,
|
||||
total_amount,
|
||||
currency: "usd".to_string(), // Default to USD
|
||||
status: PaymentStatus::default(),
|
||||
stripe_customer_id: None,
|
||||
created_at: now,
|
||||
completed_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payment_intent_id(mut self, payment_intent_id: String) -> Self {
|
||||
self.payment_intent_id = payment_intent_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = company_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn payment_plan(mut self, payment_plan: String) -> Self {
|
||||
self.payment_plan = payment_plan;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn setup_fee(mut self, setup_fee: f64) -> Self {
|
||||
self.setup_fee = setup_fee;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn monthly_fee(mut self, monthly_fee: f64) -> Self {
|
||||
self.monthly_fee = monthly_fee;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn total_amount(mut self, total_amount: f64) -> Self {
|
||||
self.total_amount = total_amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: PaymentStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stripe_customer_id(mut self, stripe_customer_id: Option<String>) -> Self {
|
||||
self.stripe_customer_id = stripe_customer_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn currency(mut self, currency: String) -> Self {
|
||||
self.currency = currency;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_at(mut self, created_at: i64) -> Self {
|
||||
self.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn completed_at(mut self, completed_at: Option<i64>) -> Self {
|
||||
self.completed_at = completed_at;
|
||||
self
|
||||
}
|
||||
|
||||
// --- Business Logic Methods ---
|
||||
|
||||
/// Complete the payment with optional Stripe customer ID
|
||||
pub fn complete_payment(mut self, stripe_customer_id: Option<String>) -> Self {
|
||||
self.status = PaymentStatus::Completed;
|
||||
self.stripe_customer_id = stripe_customer_id;
|
||||
self.completed_at = Some(chrono::Utc::now().timestamp());
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark payment as processing
|
||||
pub fn process_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Processing;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark payment as failed
|
||||
pub fn fail_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Failed;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Refund the payment
|
||||
pub fn refund_payment(mut self) -> Self {
|
||||
self.status = PaymentStatus::Refunded;
|
||||
self.base_data.update_modified();
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if payment is completed
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.status == PaymentStatus::Completed
|
||||
}
|
||||
|
||||
/// Check if payment is pending
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.status == PaymentStatus::Pending
|
||||
}
|
||||
|
||||
/// Check if payment is processing
|
||||
pub fn is_processing(&self) -> bool {
|
||||
self.status == PaymentStatus::Processing
|
||||
}
|
||||
|
||||
/// Check if payment has failed
|
||||
pub fn has_failed(&self) -> bool {
|
||||
self.status == PaymentStatus::Failed
|
||||
}
|
||||
|
||||
/// Check if payment is refunded
|
||||
pub fn is_refunded(&self) -> bool {
|
||||
self.status == PaymentStatus::Refunded
|
||||
}
|
||||
|
||||
// Setter for base_data fields if needed directly
|
||||
pub fn set_base_created_at(mut self, created_at: i64) -> Self {
|
||||
self.base_data.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, modified_at: i64) -> Self {
|
||||
self.base_data.modified_at = modified_at;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for Payment model are located in tests/payment.rs
|
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@ pub mod projects;
|
||||
pub use core::Comment;
|
||||
pub use userexample::User;
|
||||
// pub use productexample::Product; // Temporarily remove
|
||||
pub use biz::{Sale, SaleItem, SaleStatus};
|
||||
pub use biz::{Payment, PaymentStatus, Sale, SaleItem, SaleStatus};
|
||||
pub use calendar::{AttendanceStatus, Attendee, Calendar, Event};
|
||||
pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
|
||||
pub use finance::{Account, Asset, AssetType};
|
||||
|
313
heromodels/tests/payment.rs
Normal file
313
heromodels/tests/payment.rs
Normal file
@ -0,0 +1,313 @@
|
||||
use heromodels::db::Collection;
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment, PaymentStatus};
|
||||
use heromodels_core::Model;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn create_test_db() -> Arc<OurDB> {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = format!("/tmp/payment_test_{}", timestamp);
|
||||
|
||||
// Clean up any existing database at this path
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
|
||||
Arc::new(OurDB::new(path, true).expect("Failed to create test database"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_creation() {
|
||||
let payment = Payment::new(
|
||||
"pi_test_123".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
assert_eq!(payment.payment_intent_id, "pi_test_123");
|
||||
assert_eq!(payment.company_id, 1);
|
||||
assert_eq!(payment.payment_plan, "monthly");
|
||||
assert_eq!(payment.setup_fee, 100.0);
|
||||
assert_eq!(payment.monthly_fee, 50.0);
|
||||
assert_eq!(payment.total_amount, 150.0);
|
||||
assert_eq!(payment.currency, "usd");
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert_eq!(payment.stripe_customer_id, None);
|
||||
assert!(payment.created_at > 0);
|
||||
assert_eq!(payment.completed_at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_default() {
|
||||
let payment = Payment::new(
|
||||
"pi_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
500.0,
|
||||
99.0,
|
||||
1688.0,
|
||||
);
|
||||
|
||||
assert_eq!(payment.status, PaymentStatus::Pending);
|
||||
assert!(payment.is_pending());
|
||||
assert!(!payment.is_processing());
|
||||
assert!(!payment.is_completed());
|
||||
assert!(!payment.has_failed());
|
||||
assert!(!payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_processing() {
|
||||
let payment = Payment::new(
|
||||
"pi_processing_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
150.0,
|
||||
60.0,
|
||||
210.0,
|
||||
);
|
||||
|
||||
let processing_payment = payment.process_payment();
|
||||
|
||||
assert_eq!(processing_payment.status, PaymentStatus::Processing);
|
||||
assert!(processing_payment.is_processing());
|
||||
assert!(!processing_payment.is_pending());
|
||||
assert!(!processing_payment.is_completed());
|
||||
assert!(!processing_payment.has_failed());
|
||||
assert!(!processing_payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_completion() {
|
||||
let payment = Payment::new(
|
||||
"pi_complete_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
200.0,
|
||||
75.0,
|
||||
275.0,
|
||||
);
|
||||
|
||||
let stripe_customer_id = Some("cus_test_123".to_string());
|
||||
let completed_payment = payment.complete_payment(stripe_customer_id.clone());
|
||||
|
||||
assert_eq!(completed_payment.status, PaymentStatus::Completed);
|
||||
assert_eq!(completed_payment.stripe_customer_id, stripe_customer_id);
|
||||
assert!(completed_payment.is_completed());
|
||||
assert!(!completed_payment.is_pending());
|
||||
assert!(!completed_payment.has_failed());
|
||||
assert!(!completed_payment.is_refunded());
|
||||
assert!(completed_payment.completed_at.is_some());
|
||||
assert!(completed_payment.completed_at.unwrap() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_failure() {
|
||||
let payment = Payment::new(
|
||||
"pi_fail_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
300.0,
|
||||
60.0,
|
||||
1020.0,
|
||||
);
|
||||
|
||||
let failed_payment = payment.fail_payment();
|
||||
|
||||
assert_eq!(failed_payment.status, PaymentStatus::Failed);
|
||||
assert!(failed_payment.has_failed());
|
||||
assert!(!failed_payment.is_completed());
|
||||
assert!(!failed_payment.is_pending());
|
||||
assert!(!failed_payment.is_refunded());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_refund() {
|
||||
let payment = Payment::new(
|
||||
"pi_refund_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
150.0,
|
||||
45.0,
|
||||
195.0,
|
||||
);
|
||||
|
||||
// First complete the payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_refund_test".to_string()));
|
||||
assert!(completed_payment.is_completed());
|
||||
|
||||
// Then refund it
|
||||
let refunded_payment = completed_payment.refund_payment();
|
||||
|
||||
assert_eq!(refunded_payment.status, PaymentStatus::Refunded);
|
||||
assert!(refunded_payment.is_refunded());
|
||||
assert!(!refunded_payment.is_completed());
|
||||
assert!(!refunded_payment.is_pending());
|
||||
assert!(!refunded_payment.has_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_builder_pattern() {
|
||||
let custom_timestamp = 1640995200; // Jan 1, 2022
|
||||
let payment = Payment::new(
|
||||
"pi_builder_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
)
|
||||
.payment_plan("yearly".to_string())
|
||||
.setup_fee(500.0)
|
||||
.monthly_fee(99.0)
|
||||
.total_amount(1688.0)
|
||||
.currency("eur".to_string())
|
||||
.stripe_customer_id(Some("cus_builder_test".to_string()))
|
||||
.created_at(custom_timestamp)
|
||||
.completed_at(Some(custom_timestamp + 3600));
|
||||
|
||||
assert_eq!(payment.payment_plan, "yearly");
|
||||
assert_eq!(payment.setup_fee, 500.0);
|
||||
assert_eq!(payment.monthly_fee, 99.0);
|
||||
assert_eq!(payment.total_amount, 1688.0);
|
||||
assert_eq!(payment.currency, "eur");
|
||||
assert_eq!(
|
||||
payment.stripe_customer_id,
|
||||
Some("cus_builder_test".to_string())
|
||||
);
|
||||
assert_eq!(payment.created_at, custom_timestamp);
|
||||
assert_eq!(payment.completed_at, Some(custom_timestamp + 3600));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_database_persistence() {
|
||||
let db = create_test_db();
|
||||
|
||||
let payment = Payment::new(
|
||||
"pi_db_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
200.0,
|
||||
60.0,
|
||||
260.0,
|
||||
);
|
||||
|
||||
// Save payment
|
||||
let (payment_id, saved_payment) = db.set(&payment).expect("Failed to save payment");
|
||||
assert!(payment_id > 0);
|
||||
assert_eq!(saved_payment.payment_intent_id, "pi_db_test");
|
||||
|
||||
// Retrieve payment
|
||||
let retrieved_payment: Payment = db
|
||||
.get_by_id(payment_id)
|
||||
.expect("Failed to get payment")
|
||||
.unwrap();
|
||||
assert_eq!(retrieved_payment.payment_intent_id, "pi_db_test");
|
||||
assert_eq!(retrieved_payment.company_id, 1);
|
||||
assert_eq!(retrieved_payment.status, PaymentStatus::Pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_status_transitions() {
|
||||
let db = create_test_db();
|
||||
|
||||
let payment = Payment::new(
|
||||
"pi_transition_test".to_string(),
|
||||
1,
|
||||
"yearly".to_string(),
|
||||
400.0,
|
||||
80.0,
|
||||
1360.0,
|
||||
);
|
||||
|
||||
let (payment_id, mut payment) = db.set(&payment).expect("Failed to save payment");
|
||||
|
||||
// Test pending -> completed
|
||||
payment = payment.complete_payment(Some("cus_transition_test".to_string()));
|
||||
let (_, mut payment) = db.set(&payment).expect("Failed to update payment");
|
||||
assert!(payment.is_completed());
|
||||
|
||||
// Test completed -> refunded
|
||||
payment = payment.refund_payment();
|
||||
let (_, payment) = db.set(&payment).expect("Failed to update payment");
|
||||
assert!(payment.is_refunded());
|
||||
|
||||
// Verify final state in database
|
||||
let final_payment: Payment = db
|
||||
.get_by_id(payment_id)
|
||||
.expect("Failed to get payment")
|
||||
.unwrap();
|
||||
assert_eq!(final_payment.status, PaymentStatus::Refunded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payment_timestamps() {
|
||||
let payment = Payment::new(
|
||||
"pi_timestamp_test".to_string(),
|
||||
1,
|
||||
"monthly".to_string(),
|
||||
100.0,
|
||||
50.0,
|
||||
150.0,
|
||||
);
|
||||
|
||||
let initial_created_at = payment.base_data.created_at;
|
||||
let initial_modified_at = payment.base_data.modified_at;
|
||||
|
||||
// Complete payment (should update modified_at)
|
||||
let completed_payment = payment.complete_payment(Some("cus_timestamp_test".to_string()));
|
||||
|
||||
assert_eq!(completed_payment.base_data.created_at, initial_created_at);
|
||||
assert!(completed_payment.base_data.modified_at >= initial_modified_at);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_company_payment_integration() {
|
||||
let db = create_test_db();
|
||||
|
||||
// Create company with default PendingPayment status
|
||||
let company = Company::new(
|
||||
"Integration Test Corp".to_string(),
|
||||
"ITC-001".to_string(),
|
||||
chrono::Utc::now().timestamp(),
|
||||
)
|
||||
.email("test@integration.com".to_string())
|
||||
.business_type(BusinessType::Starter);
|
||||
|
||||
let (company_id, company) = db.set(&company).expect("Failed to save company");
|
||||
assert_eq!(company.status, CompanyStatus::PendingPayment);
|
||||
|
||||
// Create payment for the company
|
||||
let payment = Payment::new(
|
||||
"pi_integration_test".to_string(),
|
||||
company_id,
|
||||
"monthly".to_string(),
|
||||
250.0,
|
||||
55.0,
|
||||
305.0,
|
||||
);
|
||||
|
||||
let (_payment_id, payment) = db.set(&payment).expect("Failed to save payment");
|
||||
assert_eq!(payment.company_id, company_id);
|
||||
|
||||
// Complete payment
|
||||
let completed_payment = payment.complete_payment(Some("cus_integration_test".to_string()));
|
||||
let (_, completed_payment) = db
|
||||
.set(&completed_payment)
|
||||
.expect("Failed to update payment");
|
||||
|
||||
// Update company status to Active
|
||||
let active_company = company.status(CompanyStatus::Active);
|
||||
let (_, active_company) = db.set(&active_company).expect("Failed to update company");
|
||||
|
||||
// Verify final states
|
||||
assert!(completed_payment.is_completed());
|
||||
assert_eq!(active_company.status, CompanyStatus::Active);
|
||||
|
||||
// Verify relationship
|
||||
assert_eq!(completed_payment.company_id, active_company.get_id());
|
||||
}
|
Loading…
Reference in New Issue
Block a user