diff --git a/contracts/assetsup/src/detokenization.rs b/contracts/assetsup/src/detokenization.rs index dc16c43..c6f2078 100644 --- a/contracts/assetsup/src/detokenization.rs +++ b/contracts/assetsup/src/detokenization.rs @@ -40,12 +40,13 @@ pub fn propose_detokenization(env: &Env, asset_id: u64, proposer: Address) -> Re } /// Execute detokenization if vote passed +/// This will remove all tokens from circulation and clear tokenization records pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Result<(), Error> { let store = env.storage().persistent(); // Verify asset is tokenized let key = TokenDataKey::TokenizedAsset(asset_id); - let _: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?; + let tokenized_asset: TokenizedAsset = store.get(&key).ok_or(Error::AssetNotTokenized)?; // Check if proposal is active let proposal_key = TokenDataKey::DetokenizationProposal(asset_id); @@ -64,6 +65,65 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res return Err(Error::DetokenizationNotApproved); } + // Save total supply for event before clearing + let total_supply = tokenized_asset.total_supply; + + // Clear all votes BEFORE removing TokenizedAsset (voting module needs it) + voting::clear_proposal_votes(env, asset_id, proposal_id)?; + + // Get list of all token holders before clearing + let holders_list_key = TokenDataKey::TokenHoldersList(asset_id); + let holders = store.get::<_, soroban_sdk::Vec
>(&holders_list_key) + .ok_or(Error::AssetNotTokenized)?; + + // Remove all token holder records + for holder in holders.iter() { + let holder_key = TokenDataKey::TokenHolder(asset_id, holder.clone()); + if store.has(&holder_key) { + store.remove(&holder_key); + } + + // Remove any token locks + let lock_key = TokenDataKey::TokenLockedUntil(asset_id, holder.clone()); + if store.has(&lock_key) { + store.remove(&lock_key); + } + + // Remove unclaimed dividends + let dividend_key = TokenDataKey::UnclaimedDividend(asset_id, holder); + if store.has(÷nd_key) { + store.remove(÷nd_key); + } + } + + // Remove token holders list + if store.has(&holders_list_key) { + store.remove(&holders_list_key); + } + + // Remove transfer restrictions + let restriction_key = TokenDataKey::TransferRestriction(asset_id); + if store.has(&restriction_key) { + store.remove(&restriction_key); + } + + // Remove whitelist + let whitelist_key = TokenDataKey::Whitelist(asset_id); + if store.has(&whitelist_key) { + store.remove(&whitelist_key); + } + + // Remove token metadata + let metadata_key = TokenDataKey::TokenMetadata(asset_id); + if store.has(&metadata_key) { + store.remove(&metadata_key); + } + + // Remove the tokenized asset record (this eliminates all tokens from circulation) + if store.has(&key) { + store.remove(&key); + } + // Update proposal to executed let timestamp = env.ledger().timestamp(); let executed_proposal = DetokenizationProposal::Executed(ExecutedProposal { @@ -72,13 +132,10 @@ pub fn execute_detokenization(env: &Env, asset_id: u64, proposal_id: u64) -> Res }); store.set(&proposal_key, &executed_proposal); - // Clear all votes - voting::clear_proposal_votes(env, asset_id, proposal_id)?; - - // Emit event: (asset_id, proposal_id) + // Emit event: (asset_id, proposal_id, total_supply_removed) env.events().publish( ("detokenization", "asset_detokenized"), - (asset_id, proposal_id), + (asset_id, proposal_id, total_supply), ); Ok(()) diff --git a/contracts/assetsup/src/insurance.rs b/contracts/assetsup/src/insurance.rs index 73cb731..592c9be 100644 --- a/contracts/assetsup/src/insurance.rs +++ b/contracts/assetsup/src/insurance.rs @@ -26,11 +26,10 @@ pub enum ClaimStatus { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum PolicyType { - Comprehensive, - Theft, - Damage, Liability, - BusinessInterruption, + Property, + Comprehensive, + Custom, } #[contracttype] @@ -50,6 +49,7 @@ pub struct InsurancePolicy { pub holder: Address, pub insurer: Address, pub asset_id: BytesN<32>, + pub policy_type: PolicyType, pub coverage_amount: i128, pub deductible: i128, pub premium: i128, @@ -81,22 +81,41 @@ pub enum DataKey { AssetPolicies(BytesN<32>), } +/// Create a new insurance policy with date validation and asset indexing pub fn create_policy(env: Env, policy: InsurancePolicy) -> Result<(), Error> { - policy.insurer.require_auth(); - + // Validate coverage and deductible if policy.coverage_amount <= 0 || policy.deductible >= policy.coverage_amount { return Err(Error::InvalidPayment); } + // Validate premium + if policy.premium <= 0 { + return Err(Error::InvalidPayment); + } + + // Validate dates: start_date must be before end_date + if policy.start_date >= policy.end_date { + return Err(Error::InvalidPayment); + } + + // Validate that start_date is not in the past (allow current timestamp) + let current_time = env.ledger().timestamp(); + if policy.start_date < current_time { + return Err(Error::InvalidPayment); + } + let key = DataKey::Policy(policy.policy_id.clone()); let store = env.storage().persistent(); + // Check if policy already exists if store.has(&key) { return Err(Error::AssetAlreadyExists); } + // Store the policy store.set(&key, &policy); + // Maintain asset index: add policy to asset's policy list let mut list: Vec> = store .get(&DataKey::AssetPolicies(policy.asset_id.clone())) .unwrap_or_else(|| Vec::new(&env)); @@ -108,6 +127,135 @@ pub fn create_policy(env: Env, policy: InsurancePolicy) -> Result<(), Error> { Ok(()) } +/// Cancel a policy (authorized by holder or insurer) +pub fn cancel_policy(env: Env, policy_id: BytesN<32>, caller: Address) -> Result<(), Error> { + let store = env.storage().persistent(); + let key = DataKey::Policy(policy_id.clone()); + + let mut policy: InsurancePolicy = store.get(&key).ok_or(Error::AssetNotFound)?; + + // Only holder or insurer can cancel + if caller != policy.holder && caller != policy.insurer { + return Err(Error::Unauthorized); + } + + // Validate status transition: only Active or Suspended policies can be cancelled + if policy.status != PolicyStatus::Active && policy.status != PolicyStatus::Suspended { + return Err(Error::Unauthorized); + } + + policy.status = PolicyStatus::Cancelled; + store.set(&key, &policy); + + log!(&env, "PolicyCancelled: {:?}", policy_id); + Ok(()) +} + +/// Suspend a policy (insurer only) +pub fn suspend_policy(env: Env, policy_id: BytesN<32>, insurer: Address) -> Result<(), Error> { + let store = env.storage().persistent(); + let key = DataKey::Policy(policy_id.clone()); + + let mut policy: InsurancePolicy = store.get(&key).ok_or(Error::AssetNotFound)?; + + // Only insurer can suspend + if insurer != policy.insurer { + return Err(Error::Unauthorized); + } + + // Validate status transition: only Active policies can be suspended + if policy.status != PolicyStatus::Active { + return Err(Error::Unauthorized); + } + + policy.status = PolicyStatus::Suspended; + store.set(&key, &policy); + + log!(&env, "PolicySuspended: {:?}", policy_id); + Ok(()) +} + +/// Expire a policy (permissionless, but requires end_date < current timestamp) +pub fn expire_policy(env: Env, policy_id: BytesN<32>) -> Result<(), Error> { + let store = env.storage().persistent(); + let key = DataKey::Policy(policy_id.clone()); + + let mut policy: InsurancePolicy = store.get(&key).ok_or(Error::AssetNotFound)?; + + let current_time = env.ledger().timestamp(); + + // Require that end_date has passed + if policy.end_date >= current_time { + return Err(Error::Unauthorized); + } + + // Validate status transition: only Active or Suspended policies can expire + if policy.status != PolicyStatus::Active && policy.status != PolicyStatus::Suspended { + return Err(Error::Unauthorized); + } + + policy.status = PolicyStatus::Expired; + store.set(&key, &policy); + + log!(&env, "PolicyExpired: {:?}", policy_id); + Ok(()) +} + +/// Renew a policy (insurer only) +pub fn renew_policy( + env: Env, + policy_id: BytesN<32>, + new_end_date: u64, + new_premium: i128, + insurer: Address, +) -> Result<(), Error> { + let store = env.storage().persistent(); + let key = DataKey::Policy(policy_id.clone()); + + let mut policy: InsurancePolicy = store.get(&key).ok_or(Error::AssetNotFound)?; + + // Only insurer can renew + if insurer != policy.insurer { + return Err(Error::Unauthorized); + } + + // Validate status transition: only Active or Expired policies can be renewed + if policy.status != PolicyStatus::Active && policy.status != PolicyStatus::Expired { + return Err(Error::Unauthorized); + } + + let current_time = env.ledger().timestamp(); + + // Validate new end date is in the future + if new_end_date <= current_time { + return Err(Error::InvalidPayment); + } + + // Validate new premium is positive + if new_premium <= 0 { + return Err(Error::InvalidPayment); + } + + // Update policy + policy.end_date = new_end_date; + policy.premium = new_premium; + policy.status = PolicyStatus::Active; + policy.last_payment = current_time; + + store.set(&key, &policy); + + log!(&env, "PolicyRenewed: {:?}", policy_id); + Ok(()) +} + +/// Get all policies for a specific asset +pub fn get_asset_policies(env: Env, asset_id: BytesN<32>) -> Vec> { + env.storage() + .persistent() + .get(&DataKey::AssetPolicies(asset_id)) + .unwrap_or_else(|| Vec::new(&env)) +} + pub fn file_claim(env: Env, claim: InsuranceClaim) -> Result<(), Error> { claim.claimant.require_auth(); diff --git a/contracts/assetsup/src/lib.rs b/contracts/assetsup/src/lib.rs index 2cfe454..8639201 100644 --- a/contracts/assetsup/src/lib.rs +++ b/contracts/assetsup/src/lib.rs @@ -770,4 +770,64 @@ impl AssetUpContract { pub fn is_detokenization_active(env: Env, asset_id: u64) -> Result { detokenization::is_detokenization_active(&env, asset_id) } + + // ===================== + // Insurance Policy Management + // ===================== + + /// Create a new insurance policy + pub fn create_insurance_policy( + env: Env, + policy: insurance::InsurancePolicy, + ) -> Result<(), Error> { + policy.insurer.require_auth(); + insurance::create_policy(env, policy) + } + + /// Cancel a policy (holder or insurer) + pub fn cancel_insurance_policy( + env: Env, + policy_id: BytesN<32>, + caller: Address, + ) -> Result<(), Error> { + caller.require_auth(); + insurance::cancel_policy(env, policy_id, caller) + } + + /// Suspend a policy (insurer only) + pub fn suspend_insurance_policy( + env: Env, + policy_id: BytesN<32>, + insurer: Address, + ) -> Result<(), Error> { + insurer.require_auth(); + insurance::suspend_policy(env, policy_id, insurer) + } + + /// Expire a policy (permissionless) + pub fn expire_insurance_policy(env: Env, policy_id: BytesN<32>) -> Result<(), Error> { + insurance::expire_policy(env, policy_id) + } + + /// Renew a policy (insurer only) + pub fn renew_insurance_policy( + env: Env, + policy_id: BytesN<32>, + new_end_date: u64, + new_premium: i128, + insurer: Address, + ) -> Result<(), Error> { + insurer.require_auth(); + insurance::renew_policy(env, policy_id, new_end_date, new_premium, insurer) + } + + /// Get a specific policy + pub fn get_insurance_policy(env: Env, policy_id: BytesN<32>) -> Option { + insurance::get_policy(env, policy_id) + } + + /// Get all policies for an asset + pub fn get_asset_insurance_policies(env: Env, asset_id: BytesN<32>) -> Vec> { + insurance::get_asset_policies(env, asset_id) + } } diff --git a/contracts/assetsup/src/tests/detokenization_new.rs b/contracts/assetsup/src/tests/detokenization_new.rs index ac98fa1..ba7a0b9 100644 --- a/contracts/assetsup/src/tests/detokenization_new.rs +++ b/contracts/assetsup/src/tests/detokenization_new.rs @@ -188,3 +188,83 @@ fn test_detokenization_majority_threshold() { assert!(first_execute_err); assert!(second_execute_ok); } + +#[test] +fn test_token_elimination_on_execution() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let tokenizer = Address::generate(&env); + let holder2 = Address::generate(&env); + let proposer = Address::generate(&env); + let asset_id = 1000u64; + + let (before_exists, after_exists, balance_cleared, holders_cleared) = + env.as_contract(&contract_id, || { + setup_tokenized_asset(&env, asset_id, &tokenizer); + + // Transfer some tokens to create multiple holders + tokenization::transfer_tokens(&env, asset_id, tokenizer.clone(), holder2.clone(), 300) + .unwrap(); + + // Verify asset exists before detokenization + let before_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok(); + + // Propose detokenization + let proposal_id = + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap(); + + // Both holders vote (100%) + voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap(); + voting::cast_vote(&env, asset_id, proposal_id, holder2.clone()).unwrap(); + + // Execute detokenization + detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap(); + + // Verify tokens are removed from circulation + let after_exists = tokenization::get_tokenized_asset(&env, asset_id).is_ok(); + + // Verify balances are cleared + let balance1 = tokenization::get_token_balance(&env, asset_id, tokenizer.clone()); + let balance2 = tokenization::get_token_balance(&env, asset_id, holder2.clone()); + let balance_cleared = balance1.unwrap_or(0) == 0 && balance2.unwrap_or(0) == 0; + + // Verify holders list is cleared + let holders_result = tokenization::get_token_holders(&env, asset_id); + let holders_cleared = holders_result.is_err(); + + (before_exists, after_exists, balance_cleared, holders_cleared) + }); + + // Assert asset existed before + assert!(before_exists); + // Assert asset no longer exists after detokenization + assert!(!after_exists); + // Assert balances are cleared + assert!(balance_cleared); + // Assert holders list is cleared + assert!(holders_cleared); +} + +#[test] +fn test_cannot_propose_after_execution() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let tokenizer = Address::generate(&env); + let proposer = Address::generate(&env); + let asset_id = 1000u64; + + let second_proposal_err = env.as_contract(&contract_id, || { + setup_tokenized_asset(&env, asset_id, &tokenizer); + + // Propose and execute detokenization + let proposal_id = + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).unwrap(); + voting::cast_vote(&env, asset_id, proposal_id, tokenizer.clone()).unwrap(); + detokenization::execute_detokenization(&env, asset_id, proposal_id).unwrap(); + + // Try to propose again after execution - should fail because asset is not tokenized + detokenization::propose_detokenization(&env, asset_id, proposer.clone()).is_err() + }); + + assert!(second_proposal_err); +} diff --git a/contracts/assetsup/src/tests/insurance_new.rs b/contracts/assetsup/src/tests/insurance_new.rs new file mode 100644 index 0000000..6f98605 --- /dev/null +++ b/contracts/assetsup/src/tests/insurance_new.rs @@ -0,0 +1,434 @@ +#![cfg(test)] + +extern crate std; + +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{Address, BytesN, Env}; + +use crate::insurance::{self, InsurancePolicy, PolicyStatus, PolicyType}; +use crate::AssetUpContract; + +fn create_test_policy( + env: &Env, + policy_id: BytesN<32>, + holder: Address, + insurer: Address, + asset_id: BytesN<32>, +) -> InsurancePolicy { + let current_time = env.ledger().timestamp(); + InsurancePolicy { + policy_id, + holder, + insurer, + asset_id, + policy_type: PolicyType::Comprehensive, + coverage_amount: 100000, + deductible: 5000, + premium: 1000, + start_date: current_time, + end_date: current_time + 365 * 24 * 60 * 60, // 1 year from now + status: PolicyStatus::Active, + auto_renew: false, + last_payment: current_time, + } +} + +#[test] +fn test_create_policy_success() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let result = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + insurance::create_policy(env.clone(), policy) + }); + + assert!(result.is_ok()); +} + +#[test] +fn test_create_policy_invalid_dates() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let result = env.as_contract(&contract_id, || { + let mut policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + // Set end_date same as start_date (invalid - must be after) + policy.end_date = policy.start_date; + insurance::create_policy(env.clone(), policy) + }); + + assert!(result.is_err()); +} + +#[test] +fn test_create_policy_invalid_coverage() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let result = env.as_contract(&contract_id, || { + let mut policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + // Set deductible >= coverage_amount (invalid) + policy.deductible = policy.coverage_amount; + insurance::create_policy(env.clone(), policy) + }); + + assert!(result.is_err()); +} + +#[test] +fn test_cancel_policy_by_holder() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (create_ok, cancel_ok, final_status) = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder.clone(), insurer, asset_id); + let create_result = insurance::create_policy(env.clone(), policy).is_ok(); + + let cancel_result = + insurance::cancel_policy(env.clone(), policy_id.clone(), holder.clone()).is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()); + let status = final_policy.map(|p| p.status); + + (create_result, cancel_result, status) + }); + + assert!(create_ok); + assert!(cancel_ok); + assert_eq!(final_status, Some(PolicyStatus::Cancelled)); +} + +#[test] +fn test_cancel_policy_by_insurer() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (create_ok, cancel_ok, final_status) = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer.clone(), asset_id); + let create_result = insurance::create_policy(env.clone(), policy).is_ok(); + + let cancel_result = + insurance::cancel_policy(env.clone(), policy_id.clone(), insurer.clone()).is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()); + let status = final_policy.map(|p| p.status); + + (create_result, cancel_result, status) + }); + + assert!(create_ok); + assert!(cancel_ok); + assert_eq!(final_status, Some(PolicyStatus::Cancelled)); +} + +#[test] +fn test_cancel_policy_unauthorized() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let unauthorized = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let cancel_err = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + insurance::create_policy(env.clone(), policy).unwrap(); + + insurance::cancel_policy(env.clone(), policy_id.clone(), unauthorized.clone()).is_err() + }); + + assert!(cancel_err); +} + +#[test] +fn test_suspend_policy() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (create_ok, suspend_ok, final_status) = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer.clone(), asset_id); + let create_result = insurance::create_policy(env.clone(), policy).is_ok(); + + let suspend_result = + insurance::suspend_policy(env.clone(), policy_id.clone(), insurer.clone()).is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()); + let status = final_policy.map(|p| p.status); + + (create_result, suspend_result, status) + }); + + assert!(create_ok); + assert!(suspend_ok); + assert_eq!(final_status, Some(PolicyStatus::Suspended)); +} + +#[test] +fn test_suspend_policy_unauthorized() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let unauthorized = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let suspend_err = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + insurance::create_policy(env.clone(), policy).unwrap(); + + insurance::suspend_policy(env.clone(), policy_id.clone(), unauthorized.clone()).is_err() + }); + + assert!(suspend_err); +} + +#[test] +fn test_expire_policy_before_end_date() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 1000; + }); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let expire_err = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + insurance::create_policy(env.clone(), policy).unwrap(); + + // Try to expire before end_date (should fail) + insurance::expire_policy(env.clone(), policy_id.clone()).is_err() + }); + + assert!(expire_err); +} + +#[test] +fn test_expire_policy_after_end_date() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (expire_ok, final_status) = env.as_contract(&contract_id, || { + // Set initial time + env.ledger().with_mut(|li| { + li.timestamp = 1000; + }); + + let mut policy = create_test_policy(&env, policy_id.clone(), holder, insurer, asset_id); + // Set policy to expire at timestamp 2000 + policy.start_date = 1000; + policy.end_date = 2000; + insurance::create_policy(env.clone(), policy).unwrap(); + + // Advance time past end_date + env.ledger().with_mut(|li| { + li.timestamp = 2500; + }); + + let expire_result = insurance::expire_policy(env.clone(), policy_id.clone()).is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()); + let status = final_policy.map(|p| p.status); + + (expire_result, status) + }); + + assert!(expire_ok); + assert_eq!(final_status, Some(PolicyStatus::Expired)); +} + +#[test] +fn test_renew_policy() { + let env = Env::default(); + env.ledger().with_mut(|li| { + li.timestamp = 1000; + }); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (renew_ok, final_status, new_end_date, new_premium) = env.as_contract(&contract_id, || { + let policy = create_test_policy(&env, policy_id.clone(), holder, insurer.clone(), asset_id); + insurance::create_policy(env.clone(), policy).unwrap(); + + let new_end = 2000u64; + let new_prem = 1500i128; + let renew_result = insurance::renew_policy( + env.clone(), + policy_id.clone(), + new_end, + new_prem, + insurer.clone(), + ) + .is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()).unwrap(); + + ( + renew_result, + final_policy.status, + final_policy.end_date, + final_policy.premium, + ) + }); + + assert!(renew_ok); + assert_eq!(final_status, PolicyStatus::Active); + assert_eq!(new_end_date, 2000u64); + assert_eq!(new_premium, 1500i128); +} + +#[test] +fn test_renew_expired_policy() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (renew_ok, final_status) = env.as_contract(&contract_id, || { + // Set initial time + env.ledger().with_mut(|li| { + li.timestamp = 1000; + }); + + let mut policy = create_test_policy(&env, policy_id.clone(), holder, insurer.clone(), asset_id); + // Set policy to expire at timestamp 2000 + policy.start_date = 1000; + policy.end_date = 2000; + insurance::create_policy(env.clone(), policy).unwrap(); + + // Advance time past end_date + env.ledger().with_mut(|li| { + li.timestamp = 2500; + }); + + // Expire the policy first + insurance::expire_policy(env.clone(), policy_id.clone()).unwrap(); + + // Now renew it to timestamp 3500 + let renew_result = insurance::renew_policy( + env.clone(), + policy_id.clone(), + 3500, + 1500, + insurer.clone(), + ) + .is_ok(); + + let final_policy = insurance::get_policy(env.clone(), policy_id.clone()).unwrap(); + + (renew_result, final_policy.status) + }); + + assert!(renew_ok); + assert_eq!(final_status, PolicyStatus::Active); +} + +#[test] +fn test_get_asset_policies() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id1 = BytesN::from_array(&env, &[2u8; 32]); + let policy_id2 = BytesN::from_array(&env, &[3u8; 32]); + + let policy_count = env.as_contract(&contract_id, || { + // Create two policies for the same asset + let policy1 = create_test_policy( + &env, + policy_id1.clone(), + holder.clone(), + insurer.clone(), + asset_id.clone(), + ); + let policy2 = create_test_policy( + &env, + policy_id2.clone(), + holder.clone(), + insurer.clone(), + asset_id.clone(), + ); + + insurance::create_policy(env.clone(), policy1).unwrap(); + insurance::create_policy(env.clone(), policy2).unwrap(); + + let policies = insurance::get_asset_policies(env.clone(), asset_id.clone()); + policies.len() + }); + + assert_eq!(policy_count, 2); +} + +#[test] +fn test_status_transition_validation() { + let env = Env::default(); + let contract_id = env.register(AssetUpContract, ()); + let holder = Address::generate(&env); + let insurer = Address::generate(&env); + let asset_id = BytesN::from_array(&env, &[1u8; 32]); + let policy_id = BytesN::from_array(&env, &[2u8; 32]); + + let (suspend_ok, cancel_after_suspend_ok, suspend_cancelled_err) = + env.as_contract(&contract_id, || { + let policy = + create_test_policy(&env, policy_id.clone(), holder.clone(), insurer.clone(), asset_id); + insurance::create_policy(env.clone(), policy).unwrap(); + + // Active -> Suspended (should work) + let suspend_result = + insurance::suspend_policy(env.clone(), policy_id.clone(), insurer.clone()).is_ok(); + + // Suspended -> Cancelled (should work) + let cancel_result = + insurance::cancel_policy(env.clone(), policy_id.clone(), holder.clone()).is_ok(); + + // Cancelled -> Suspended (should fail) + let suspend_again_result = + insurance::suspend_policy(env.clone(), policy_id.clone(), insurer.clone()).is_err(); + + (suspend_result, cancel_result, suspend_again_result) + }); + + assert!(suspend_ok); + assert!(cancel_after_suspend_ok); + assert!(suspend_cancelled_err); +} diff --git a/contracts/assetsup/src/tests/mod.rs b/contracts/assetsup/src/tests/mod.rs index 35d6170..fd8a933 100644 --- a/contracts/assetsup/src/tests/mod.rs +++ b/contracts/assetsup/src/tests/mod.rs @@ -1,5 +1,6 @@ mod detokenization_new; mod dividends_new; +mod insurance_new; mod integration; mod tokenization_new; mod transfer_restrictions_new;