diff --git a/quicklendx-contracts/src/lib.rs b/quicklendx-contracts/src/lib.rs index 2812d8c4..fcbe6e44 100644 --- a/quicklendx-contracts/src/lib.rs +++ b/quicklendx-contracts/src/lib.rs @@ -164,6 +164,21 @@ impl QuickLendXContract { init::ProtocolInitializer::is_initialized(&env) } + /// Get the protocol/contract version + /// + /// Returns the version written during initialization, or the current + /// PROTOCOL_VERSION constant if the contract has not been initialized yet. + /// + /// # Returns + /// * `u32` - The protocol version number + /// + /// # Version Format + /// Version is a simple integer increment (e.g., 1, 2, 3...) + /// Major versions indicate breaking changes that require migration. + pub fn get_version(env: Env) -> u32 { + init::ProtocolInitializer::get_protocol_version(&env) + } + /// Initialize the admin address (deprecated: use initialize) pub fn initialize_admin(env: Env, admin: Address) -> Result<(), QuickLendXError> { admin.require_auth(); diff --git a/quicklendx-contracts/src/settlement.rs b/quicklendx-contracts/src/settlement.rs index e5d0c51f..ae55b4df 100644 --- a/quicklendx-contracts/src/settlement.rs +++ b/quicklendx-contracts/src/settlement.rs @@ -9,6 +9,8 @@ use crate::invoice::{ Invoice, InvoiceStatus, InvoiceStorage, PaymentRecord as InvoicePaymentRecord, }; use crate::notifications::NotificationSystem; +use crate::defaults::DEFAULT_GRACE_PERIOD; +use crate::events::TOPIC_INVOICE_SETTLED_FINAL; use crate::payments::transfer_funds; use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, String, Vec}; diff --git a/quicklendx-contracts/src/test_audit.rs b/quicklendx-contracts/src/test_audit.rs index dee8bb31..8efad145 100644 --- a/quicklendx-contracts/src/test_audit.rs +++ b/quicklendx-contracts/src/test_audit.rs @@ -830,6 +830,30 @@ fn test_audit_invoice_cancelled_produces_entry() { &1000i128, ¤cy, &due_date, + &String::from_str(&env, "Test invoice"), + &invoice::InvoiceCategory::Services, + &Vec::from_array(&env, [String::from_str(&env, "test")]), + ).unwrap(); + + client.cancel_invoice(&invoice_id); + + let logs = client.query_audit_logs( + &None, + &None, + &None, + &None, + &None, + &10 + ); + + assert!(logs.len() >= 1); + + // Find the cancellation entry + let cancel_entry = logs.iter().find(|log| { + log.operation == audit::AuditOperation::InvoiceCancelled + }).unwrap(); + + assert_eq!(cancel_entry.actor, business); &String::from_str(&env, "Cancel Test"), &InvoiceCategory::Services, &Vec::new(&env), diff --git a/quicklendx-contracts/src/test_init.rs b/quicklendx-contracts/src/test_init.rs index 6c61140d..36dca67e 100644 --- a/quicklendx-contracts/src/test_init.rs +++ b/quicklendx-contracts/src/test_init.rs @@ -157,3 +157,191 @@ fn test_validation_invalid_grace_period() { let result = client.try_initialize(¶ms); assert_eq!(result, Err(Ok(QuickLendXError::InvalidTimestamp))); } + +#[test] +fn test_get_version_before_initialization() { + let (env, client) = setup(); + + // Before initialization, should return the current PROTOCOL_VERSION constant + let version = client.get_version(); + assert_eq!(version, 1); + + // Contract should not be initialized yet + assert!(!client.is_initialized()); +} + +#[test] +fn test_get_version_after_initialization() { + let (env, client) = setup(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let initial_currencies = Vec::from_array(&env, [Address::generate(&env)]); + + let params = InitializationParams { + admin: admin.clone(), + treasury: treasury.clone(), + fee_bps: 200, + min_invoice_amount: 1_000_000, + max_due_date_days: 365, + grace_period_seconds: 604800, + initial_currencies: initial_currencies.clone(), + }; + + // Before initialization + let version_before = client.get_version(); + assert_eq!(version_before, 1); + + // Initialize the contract + client.initialize(¶ms); + + // After initialization, version should still be the same + let version_after = client.get_version(); + assert_eq!(version_after, 1); + + // Contract should now be initialized + assert!(client.is_initialized()); + + // Version should be consistent before and after initialization + assert_eq!(version_before, version_after); +} + +#[test] +fn test_version_immutability() { + let (env, client) = setup(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let initial_currencies = Vec::from_array(&env, [Address::generate(&env)]); + + let params = InitializationParams { + admin: admin.clone(), + treasury: treasury.clone(), + fee_bps: 200, + min_invoice_amount: 1_000_000, + max_due_date_days: 365, + grace_period_seconds: 604800, + initial_currencies: initial_currencies.clone(), + }; + + // Initialize the contract + client.initialize(¶ms); + + // Get version multiple times - should always return the same value + let version1 = client.get_version(); + let version2 = client.get_version(); + let version3 = client.get_version(); + + assert_eq!(version1, 1); + assert_eq!(version2, 1); + assert_eq!(version3, 1); + + // Version should remain constant across multiple calls + assert_eq!(version1, version2); + assert_eq!(version2, version3); +} + +#[test] +fn test_version_format_documentation() { + let (env, client) = setup(); + + // Test that version follows the documented format (simple integer) + let version = client.get_version(); + + // Version should be a positive integer + assert!(version > 0); + assert!(version <= u32::MAX); + + // Current version should be 1 based on PROTOCOL_VERSION constant + assert_eq!(version, 1); + + // Verify it's a simple integer format (not semver or complex format) + let version_str = version.to_string(); + assert!(version_str.parse::().is_ok()); +} + +#[test] +fn test_version_consistency_across_operations() { + let (env, client) = setup(); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let initial_currencies = Vec::from_array(&env, [Address::generate(&env)]); + + let params = InitializationParams { + admin: admin.clone(), + treasury: treasury.clone(), + fee_bps: 200, + min_invoice_amount: 1_000_000, + max_due_date_days: 365, + grace_period_seconds: 604800, + initial_currencies: initial_currencies.clone(), + }; + + // Get initial version + let initial_version = client.get_version(); + + // Initialize + client.initialize(¶ms); + + // Perform various operations + let current_admin = client.get_current_admin().unwrap(); + client.transfer_admin(¤t_admin, &Address::generate(&env)); + + // Add currency + let new_currency = Address::generate(&env); + client.add_currency(¤t_admin, &new_currency); + + // Version should remain unchanged throughout all operations + let final_version = client.get_version(); + assert_eq!(initial_version, final_version); + assert_eq!(final_version, 1); +} + +#[test] +fn test_version_edge_cases() { + let (env, client) = setup(); + + // Test version behavior in edge cases + + // 1. Fresh contract instance + let version1 = client.get_version(); + assert_eq!(version1, 1); + + // 2. After failed initialization attempt + let admin = Address::generate(&env); + let invalid_params = InitializationParams { + admin: admin.clone(), + treasury: Address::generate(&env), + fee_bps: 1001, // Invalid - should fail + min_invoice_amount: 1_000_000, + max_due_date_days: 365, + grace_period_seconds: 604800, + initial_currencies: Vec::new(&env), + }; + + // This should fail + let result = client.try_initialize(&invalid_params); + assert!(result.is_err()); + + // Version should still be accessible and unchanged + let version2 = client.get_version(); + assert_eq!(version2, 1); + assert_eq!(version1, version2); + + // 3. After successful initialization + let valid_params = InitializationParams { + admin: admin.clone(), + treasury: Address::generate(&env), + fee_bps: 200, + min_invoice_amount: 1_000_000, + max_due_date_days: 365, + grace_period_seconds: 604800, + initial_currencies: Vec::new(&env), + }; + + client.initialize(&valid_params); + let version3 = client.get_version(); + assert_eq!(version3, 1); + assert_eq!(version1, version3); +}